diff --git a/Makefile b/Makefile index b2a1efb2..2d9fe1b1 100644 --- a/Makefile +++ b/Makefile @@ -33,8 +33,9 @@ clean: ## Delete intermediate build artifacts .PHONY: generate generate: $(BIN)/buf $(BIN)/license-header ## Regenerate code and license headers rm -rf gen - buf generate buf.build/bufbuild/protovalidate:$(PROTOVALIDATE_VERSION) - buf generate buf.build/bufbuild/protovalidate-testing:$(PROTOVALIDATE_VERSION) + $(BIN)/buf generate buf.build/bufbuild/protovalidate:$(PROTOVALIDATE_VERSION) + $(BIN)/buf generate buf.build/bufbuild/protovalidate-testing:$(PROTOVALIDATE_VERSION) + $(BIN)/buf generate $(ADD_LICENSE_HEADER) .PHONY: format @@ -44,7 +45,7 @@ format: install $(BIN)/license-header ## Format code pipenv run ruff check --fix protovalidate tests .PHONY: test -test: $(BIN)/protovalidate-conformance generate install ## Run unit tests +test: generate install ## Run unit tests pipenv run pytest .PHONY: conformance diff --git a/buf.lock b/buf.lock new file mode 100644 index 00000000..9df54bc2 --- /dev/null +++ b/buf.lock @@ -0,0 +1,9 @@ +# Generated by buf. DO NOT EDIT. +version: v2 +deps: + - name: buf.build/bufbuild/protovalidate + commit: 0409229c37804d6187ee0806eb4eebce + digest: b5:795db9d3a6e066dc61d99ac651fa7f136171869abe2211ca272dd84aada7bc4583b9508249fa5b61300a5b1fe8b6dbf6edbc088aa0345d1ccb9fff705e3d48e9 + - name: buf.build/bufbuild/protovalidate-testing + commit: 5acbe1f3c8f24ced9466b9ccccad4cb0 + digest: b5:5e9d54d19ce3d9d368f4b1b5ee4f20094d1c33d0f2dca19536339335c2e70d5ffedbd4fa28e290b59ecae0671c9d2dc20b6b8ebba5a9ac76cbf5f9d2af655ef4 diff --git a/buf.yaml b/buf.yaml new file mode 100644 index 00000000..5fc5b920 --- /dev/null +++ b/buf.yaml @@ -0,0 +1,15 @@ +version: v2 +modules: + - path: proto +deps: + - buf.build/bufbuild/protovalidate + - buf.build/bufbuild/protovalidate-testing +lint: + use: + - STANDARD + ignore_only: + PROTOVALIDATE: + - proto/tests/example/v1/validations.proto +breaking: + use: + - FILE diff --git a/gen/tests/example/v1/validations_pb2.py b/gen/tests/example/v1/validations_pb2.py new file mode 100644 index 00000000..f9b58951 --- /dev/null +++ b/gen/tests/example/v1/validations_pb2.py @@ -0,0 +1,97 @@ +# Copyright 2023-2025 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: tests/example/v1/validations.proto +# Protobuf Python Version: 5.29.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 29, + 3, + '', + 'tests/example/v1/validations.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\"tests/example/v1/validations.proto\x12\x10tests.example.v1\x1a\x1b\x62uf/validate/validate.proto\x1a\x1fgoogle/protobuf/timestamp.proto\")\n\x0c\x44oubleFinite\x12\x19\n\x03val\x18\x01 \x01(\x01\x42\x07\xbaH\x04\x12\x02@\x01R\x03val\";\n\x0eSFixed64ExLTGT\x12)\n\x03val\x18\x01 \x01(\x10\x42\x17\xbaH\x14\x62\x12\x11\x00\x00\x00\x00\x00\x00\x00\x00!\n\x00\x00\x00\x00\x00\x00\x00R\x03val\")\n\x0cTestOneofMsg\x12\x19\n\x03val\x18\x01 \x01(\x08\x42\x07\xbaH\x04j\x02\x08\x01R\x03val\"q\n\x05Oneof\x12\x1a\n\x01x\x18\x01 \x01(\tB\n\xbaH\x07r\x05:\x03\x66ooH\x00R\x01x\x12\x17\n\x01y\x18\x02 \x01(\x05\x42\x07\xbaH\x04\x1a\x02 \x00H\x00R\x01y\x12.\n\x01z\x18\x03 \x01(\x0b\x32\x1e.tests.example.v1.TestOneofMsgH\x00R\x01zB\x03\n\x01o\"H\n\x0eTimestampGTNow\x12\x36\n\x03val\x18\x01 \x01(\x0b\x32\x1a.google.protobuf.TimestampB\x08\xbaH\x05\xb2\x01\x02@\x01R\x03val\"\x87\x01\n\tMapMinMax\x12\x42\n\x03val\x18\x01 \x03(\x0b\x32$.tests.example.v1.MapMinMax.ValEntryB\n\xbaH\x07\x9a\x01\x04\x08\x02\x10\x04R\x03val\x1a\x36\n\x08ValEntry\x12\x10\n\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n\x05value\x18\x02 \x01(\x08R\x05value:\x02\x38\x01\"\x85\x01\n\x07MapKeys\x12\x42\n\x03val\x18\x01 \x03(\x0b\x32\".tests.example.v1.MapKeys.ValEntryB\x0c\xbaH\t\x9a\x01\x06\"\x04\x42\x02\x10\x00R\x03val\x1a\x36\n\x08ValEntry\x12\x10\n\x03key\x18\x01 \x01(\x12R\x03key\x12\x14\n\x05value\x18\x02 \x01(\tR\x05value:\x02\x38\x01\"\"\n\x05\x45mbed\x12\x19\n\x03val\x18\x01 \x01(\x03\x42\x07\xbaH\x04\"\x02 \x00R\x03val\"K\n\x11RepeatedEmbedSkip\x12\x36\n\x03val\x18\x01 \x03(\x0b\x32\x17.tests.example.v1.EmbedB\x0b\xbaH\x08\x92\x01\x05\"\x03\xd8\x01\x03R\x03valB\x8a\x01\n\x14\x63om.tests.example.v1B\x10ValidationsProtoP\x01\xa2\x02\x03TEX\xaa\x02\x10Tests.Example.V1\xca\x02\x10Tests\\Example\\V1\xe2\x02\x1cTests\\Example\\V1\\GPBMetadata\xea\x02\x12Tests::Example::V1b\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'tests.example.v1.validations_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\024com.tests.example.v1B\020ValidationsProtoP\001\242\002\003TEX\252\002\020Tests.Example.V1\312\002\020Tests\\Example\\V1\342\002\034Tests\\Example\\V1\\GPBMetadata\352\002\022Tests::Example::V1' + _globals['_DOUBLEFINITE'].fields_by_name['val']._loaded_options = None + _globals['_DOUBLEFINITE'].fields_by_name['val']._serialized_options = b'\272H\004\022\002@\001' + _globals['_SFIXED64EXLTGT'].fields_by_name['val']._loaded_options = None + _globals['_SFIXED64EXLTGT'].fields_by_name['val']._serialized_options = b'\272H\024b\022\021\000\000\000\000\000\000\000\000!\n\000\000\000\000\000\000\000' + _globals['_TESTONEOFMSG'].fields_by_name['val']._loaded_options = None + _globals['_TESTONEOFMSG'].fields_by_name['val']._serialized_options = b'\272H\004j\002\010\001' + _globals['_ONEOF'].fields_by_name['x']._loaded_options = None + _globals['_ONEOF'].fields_by_name['x']._serialized_options = b'\272H\007r\005:\003foo' + _globals['_ONEOF'].fields_by_name['y']._loaded_options = None + _globals['_ONEOF'].fields_by_name['y']._serialized_options = b'\272H\004\032\002 \000' + _globals['_TIMESTAMPGTNOW'].fields_by_name['val']._loaded_options = None + _globals['_TIMESTAMPGTNOW'].fields_by_name['val']._serialized_options = b'\272H\005\262\001\002@\001' + _globals['_MAPMINMAX_VALENTRY']._loaded_options = None + _globals['_MAPMINMAX_VALENTRY']._serialized_options = b'8\001' + _globals['_MAPMINMAX'].fields_by_name['val']._loaded_options = None + _globals['_MAPMINMAX'].fields_by_name['val']._serialized_options = b'\272H\007\232\001\004\010\002\020\004' + _globals['_MAPKEYS_VALENTRY']._loaded_options = None + _globals['_MAPKEYS_VALENTRY']._serialized_options = b'8\001' + _globals['_MAPKEYS'].fields_by_name['val']._loaded_options = None + _globals['_MAPKEYS'].fields_by_name['val']._serialized_options = b'\272H\t\232\001\006\"\004B\002\020\000' + _globals['_EMBED'].fields_by_name['val']._loaded_options = None + _globals['_EMBED'].fields_by_name['val']._serialized_options = b'\272H\004\"\002 \000' + _globals['_REPEATEDEMBEDSKIP'].fields_by_name['val']._loaded_options = None + _globals['_REPEATEDEMBEDSKIP'].fields_by_name['val']._serialized_options = b'\272H\010\222\001\005\"\003\330\001\003' + _globals['_DOUBLEFINITE']._serialized_start=118 + _globals['_DOUBLEFINITE']._serialized_end=159 + _globals['_SFIXED64EXLTGT']._serialized_start=161 + _globals['_SFIXED64EXLTGT']._serialized_end=220 + _globals['_TESTONEOFMSG']._serialized_start=222 + _globals['_TESTONEOFMSG']._serialized_end=263 + _globals['_ONEOF']._serialized_start=265 + _globals['_ONEOF']._serialized_end=378 + _globals['_TIMESTAMPGTNOW']._serialized_start=380 + _globals['_TIMESTAMPGTNOW']._serialized_end=452 + _globals['_MAPMINMAX']._serialized_start=455 + _globals['_MAPMINMAX']._serialized_end=590 + _globals['_MAPMINMAX_VALENTRY']._serialized_start=536 + _globals['_MAPMINMAX_VALENTRY']._serialized_end=590 + _globals['_MAPKEYS']._serialized_start=593 + _globals['_MAPKEYS']._serialized_end=726 + _globals['_MAPKEYS_VALENTRY']._serialized_start=672 + _globals['_MAPKEYS_VALENTRY']._serialized_end=726 + _globals['_EMBED']._serialized_start=728 + _globals['_EMBED']._serialized_end=762 + _globals['_REPEATEDEMBEDSKIP']._serialized_start=764 + _globals['_REPEATEDEMBEDSKIP']._serialized_end=839 +# @@protoc_insertion_point(module_scope) diff --git a/gen/tests/example/v1/validations_pb2.pyi b/gen/tests/example/v1/validations_pb2.pyi new file mode 100644 index 00000000..576c477e --- /dev/null +++ b/gen/tests/example/v1/validations_pb2.pyi @@ -0,0 +1,94 @@ +# Copyright 2023-2025 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from buf.validate import validate_pb2 as _validate_pb2 +from google.protobuf import timestamp_pb2 as _timestamp_pb2 +from google.protobuf.internal import containers as _containers +from google.protobuf import descriptor as _descriptor +from google.protobuf import message as _message +from typing import ClassVar as _ClassVar, Iterable as _Iterable, Mapping as _Mapping, Optional as _Optional, Union as _Union + +DESCRIPTOR: _descriptor.FileDescriptor + +class DoubleFinite(_message.Message): + __slots__ = ("val",) + VAL_FIELD_NUMBER: _ClassVar[int] + val: float + def __init__(self, val: _Optional[float] = ...) -> None: ... + +class SFixed64ExLTGT(_message.Message): + __slots__ = ("val",) + VAL_FIELD_NUMBER: _ClassVar[int] + val: int + def __init__(self, val: _Optional[int] = ...) -> None: ... + +class TestOneofMsg(_message.Message): + __slots__ = ("val",) + VAL_FIELD_NUMBER: _ClassVar[int] + val: bool + def __init__(self, val: bool = ...) -> None: ... + +class Oneof(_message.Message): + __slots__ = ("x", "y", "z") + X_FIELD_NUMBER: _ClassVar[int] + Y_FIELD_NUMBER: _ClassVar[int] + Z_FIELD_NUMBER: _ClassVar[int] + x: str + y: int + z: TestOneofMsg + def __init__(self, x: _Optional[str] = ..., y: _Optional[int] = ..., z: _Optional[_Union[TestOneofMsg, _Mapping]] = ...) -> None: ... + +class TimestampGTNow(_message.Message): + __slots__ = ("val",) + VAL_FIELD_NUMBER: _ClassVar[int] + val: _timestamp_pb2.Timestamp + def __init__(self, val: _Optional[_Union[_timestamp_pb2.Timestamp, _Mapping]] = ...) -> None: ... + +class MapMinMax(_message.Message): + __slots__ = ("val",) + class ValEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: str + value: bool + def __init__(self, key: _Optional[str] = ..., value: bool = ...) -> None: ... + VAL_FIELD_NUMBER: _ClassVar[int] + val: _containers.ScalarMap[str, bool] + def __init__(self, val: _Optional[_Mapping[str, bool]] = ...) -> None: ... + +class MapKeys(_message.Message): + __slots__ = ("val",) + class ValEntry(_message.Message): + __slots__ = ("key", "value") + KEY_FIELD_NUMBER: _ClassVar[int] + VALUE_FIELD_NUMBER: _ClassVar[int] + key: int + value: str + def __init__(self, key: _Optional[int] = ..., value: _Optional[str] = ...) -> None: ... + VAL_FIELD_NUMBER: _ClassVar[int] + val: _containers.ScalarMap[int, str] + def __init__(self, val: _Optional[_Mapping[int, str]] = ...) -> None: ... + +class Embed(_message.Message): + __slots__ = ("val",) + VAL_FIELD_NUMBER: _ClassVar[int] + val: int + def __init__(self, val: _Optional[int] = ...) -> None: ... + +class RepeatedEmbedSkip(_message.Message): + __slots__ = ("val",) + VAL_FIELD_NUMBER: _ClassVar[int] + val: _containers.RepeatedCompositeFieldContainer[Embed] + def __init__(self, val: _Optional[_Iterable[_Union[Embed, _Mapping]]] = ...) -> None: ... diff --git a/proto/tests/example/v1/validations.proto b/proto/tests/example/v1/validations.proto new file mode 100644 index 00000000..4fff574d --- /dev/null +++ b/proto/tests/example/v1/validations.proto @@ -0,0 +1,65 @@ +// Copyright 2023-2025 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +package tests.example.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +message DoubleFinite { + double val = 1 [(buf.validate.field).double.finite = true]; +} + +message SFixed64ExLTGT { + sfixed64 val = 1 [(buf.validate.field).sfixed64 = { + lt: 0 + gt: 10 + }]; +} + +message TestOneofMsg { + bool val = 1 [(buf.validate.field).bool.const = true]; +} + +message Oneof { + oneof o { + string x = 1 [(buf.validate.field).string.prefix = "foo"]; + int32 y = 2 [(buf.validate.field).int32.gt = 0]; + TestOneofMsg z = 3; + } +} + +message TimestampGTNow { + google.protobuf.Timestamp val = 1 [(buf.validate.field).timestamp.gt_now = true]; +} + +message MapMinMax { + map val = 1 [(buf.validate.field).map = { + min_pairs: 2 + max_pairs: 4 + }]; +} + +message MapKeys { + map val = 1 [(buf.validate.field).map.keys.sint64.lt = 0]; +} + +message Embed { + int64 val = 1 [(buf.validate.field).int64.gt = 0]; +} +message RepeatedEmbedSkip { + repeated Embed val = 1 [(buf.validate.field).repeated.items.ignore = IGNORE_ALWAYS]; +} diff --git a/tests/validate_test.py b/tests/validate_test.py index e95aa9e8..82899089 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -15,12 +15,12 @@ import unittest import protovalidate -from buf.validate.conformance.cases import maps_pb2, numbers_pb2, oneofs_pb2, repeated_pb2, wkt_timestamp_pb2 +from gen.tests.example.v1 import validations_pb2 class TestValidate(unittest.TestCase): def test_ninf(self): - msg = numbers_pb2.DoubleFinite() + msg = validations_pb2.DoubleFinite() msg.val = float("-inf") violations = protovalidate.collect_violations(msg) self.assertEqual(len(violations), 1) @@ -29,7 +29,7 @@ def test_ninf(self): self.assertEqual(violations[0].rule_value, True) def test_map_key(self): - msg = maps_pb2.MapKeys() + msg = validations_pb2.MapKeys() msg.val[1] = "a" violations = protovalidate.collect_violations(msg) self.assertEqual(len(violations), 1) @@ -38,18 +38,18 @@ def test_map_key(self): self.assertEqual(violations[0].rule_value, 0) def test_sfixed64(self): - msg = numbers_pb2.SFixed64ExLTGT(val=11) + msg = validations_pb2.SFixed64ExLTGT(val=11) protovalidate.validate(msg) violations = protovalidate.collect_violations(msg) self.assertEqual(len(violations), 0) def test_oneofs(self): - msg1 = oneofs_pb2.Oneof() + msg1 = validations_pb2.Oneof() msg1.y = 123 protovalidate.validate(msg1) - msg2 = oneofs_pb2.Oneof() + msg2 = validations_pb2.Oneof() msg2.z.val = True protovalidate.validate(msg2) @@ -58,7 +58,7 @@ def test_oneofs(self): assert len(violations) == 0 def test_repeated(self): - msg = repeated_pb2.RepeatedEmbedSkip() + msg = validations_pb2.RepeatedEmbedSkip() msg.val.add(val=-1) protovalidate.validate(msg) @@ -66,7 +66,7 @@ def test_repeated(self): assert len(violations) == 0 def test_maps(self): - msg = maps_pb2.MapMinMax() + msg = validations_pb2.MapMinMax() try: protovalidate.validate(msg) except protovalidate.ValidationError as e: @@ -78,7 +78,7 @@ def test_maps(self): assert len(violations) == 1 def test_timestamp(self): - msg = wkt_timestamp_pb2.TimestampGTNow() + msg = validations_pb2.TimestampGTNow() protovalidate.validate(msg) violations = protovalidate.collect_violations(msg)