diff options
author | Sara Golemon <sara.golemon@mongodb.com> | 2018-09-26 15:17:56 +0000 |
---|---|---|
committer | Sara Golemon <sara.golemon@mongodb.com> | 2018-10-05 23:33:33 +0000 |
commit | f9600749453db5dec58bd1bbfd967e16b1578a24 (patch) | |
tree | 22e0f6e8f773483ad12828d92f4bf8d1496fbe70 | |
parent | 2db04b524dc5e2121b74829814db7c8e84e5696d (diff) | |
download | mongo-f9600749453db5dec58bd1bbfd967e16b1578a24.tar.gz |
SERVER-37168 Add validators for IDL fields
-rw-r--r-- | buildscripts/idl/idl/ast.py | 27 | ||||
-rw-r--r-- | buildscripts/idl/idl/binder.py | 33 | ||||
-rw-r--r-- | buildscripts/idl/idl/errors.py | 9 | ||||
-rw-r--r-- | buildscripts/idl/idl/generator.py | 109 | ||||
-rw-r--r-- | buildscripts/idl/idl/parser.py | 18 | ||||
-rw-r--r-- | buildscripts/idl/idl/syntax.py | 25 | ||||
-rw-r--r-- | src/mongo/idl/idl_test.cpp | 208 | ||||
-rw-r--r-- | src/mongo/idl/idl_test.h | 70 | ||||
-rw-r--r-- | src/mongo/idl/unittest.idl | 63 |
9 files changed, 557 insertions, 5 deletions
diff --git a/buildscripts/idl/idl/ast.py b/buildscripts/idl/idl/ast.py index e5eda3b8426..7b2c85fda1f 100644 --- a/buildscripts/idl/idl/ast.py +++ b/buildscripts/idl/idl/ast.py @@ -92,6 +92,30 @@ class Struct(common.SourceLocation): super(Struct, self).__init__(file_name, line, column) +class Validator(common.SourceLocation): + """ + An instance of a validator for a field. + + The validator must include at least one of the defined validation predicates. + If more than one is included, they must ALL evaluate to true. + """ + + # pylint: disable=too-many-instance-attributes + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a Validator.""" + # Don't lint gt/lt as bad attribute names. + # pylint: disable=C0103 + self.gt = None # type: Optional[Union[int, float]] + self.lt = None # type: Optional[Union[int, float]] + self.gte = None # type: Optional[Union[int, float]] + self.lte = None # type: Optional[Union[int, float]] + self.callback = None # type: Optional[unicode] + + super(Validator, self).__init__(file_name, line, column) + + class Field(common.SourceLocation): """ An instance of a field in a struct. @@ -139,6 +163,9 @@ class Field(common.SourceLocation): self.serialize_op_msg_request_only = False # type: bool self.constructed = False # type: bool + # Validation rules. + self.validator = None # type: Optional[Validator] + super(Field, self).__init__(file_name, line, column) diff --git a/buildscripts/idl/idl/binder.py b/buildscripts/idl/idl/binder.py index bf8c1881514..02955009e73 100644 --- a/buildscripts/idl/idl/binder.py +++ b/buildscripts/idl/idl/binder.py @@ -505,6 +505,34 @@ def _normalize_method_name(cpp_type_name, cpp_method_name): return cpp_method_name +def _bind_validator(ctxt, validator): + # type: (errors.ParserContext, syntax.Validator) -> ast.Validator + """Bind a validator from the idl.syntax tree.""" + + ast_validator = ast.Validator(validator.file_name, validator.line, validator.column) + + # Parse syntax value as numeric if possible. + for pred in ["gt", "lt", "gte", "lte"]: + val = getattr(validator, pred) + if val is None: + continue + + try: + intval = int(val) + if (intval < -0x80000000) or (intval > 0x7FFFFFFF): + raise ValueError('IDL ints are limited to int32_t') + setattr(ast_validator, pred, intval) + except ValueError: + try: + setattr(ast_validator, pred, float(val)) + except ValueError: + ctxt.add_value_not_numeric_error(ast_validator, pred, val) + return None + + ast_validator.callback = validator.callback + return ast_validator + + def _bind_field(ctxt, parsed_spec, field): # type: (errors.ParserContext, syntax.IDLSpec, syntax.Field) -> ast.Field """ @@ -598,6 +626,11 @@ def _bind_field(ctxt, parsed_spec, field): # Validation doc_sequence types _validate_doc_sequence_field(ctxt, ast_field) + if field.validator is not None: + ast_field.validator = _bind_validator(ctxt, field.validator) + if ast_field.validator is None: + return None + return ast_field diff --git a/buildscripts/idl/idl/errors.py b/buildscripts/idl/idl/errors.py index 9fc78547f8c..b9785c2bf66 100644 --- a/buildscripts/idl/idl/errors.py +++ b/buildscripts/idl/idl/errors.py @@ -85,6 +85,7 @@ ERROR_ID_IS_NODE_VALID_INT = "ID0049" ERROR_ID_IS_NODE_VALID_NON_NEGATIVE_INT = "ID0050" ERROR_ID_IS_DUPLICATE_COMPARISON_ORDER = "ID0051" ERROR_ID_IS_COMMAND_TYPE_EXTRANEOUS = "ID0052" +ERROR_ID_VALUE_NOT_NUMERIC = "ID0053" class IDLError(Exception): @@ -665,6 +666,14 @@ class ParserContext(object): ("Command '%s' cannot have a 'type' property unless namespace equals 'type'.") % (command_name)) + def add_value_not_numeric_error(self, location, attrname, value): + # type: (common.SourceLocation, unicode, unicode) -> None + """Add an error about non-numeric value where number expected.""" + # pylint: disable=invalid-name + self._add_error(location, ERROR_ID_VALUE_NOT_NUMERIC, + ("'%s' requires a numeric value, but %s can not be cast") % (attrname, + value)) + def _assert_unique_error_messages(): # type: () -> None diff --git a/buildscripts/idl/idl/generator.py b/buildscripts/idl/idl/generator.py index dd8349dea2e..3da969c756b 100644 --- a/buildscripts/idl/idl/generator.py +++ b/buildscripts/idl/idl/generator.py @@ -76,6 +76,16 @@ def _get_field_constant_name(field): field.cpp_name)) +def _get_field_member_validator_name(field): + # type (ast.Field) -> unicode + """ + Get the name of the validator method for this field. + + Fields with no validation rules will have a stub validator which returns Status::OK(). + """ + return 'validate%s' % common.title_case(field.cpp_name) + + def _access_member(field): # type: (ast.Field) -> unicode """Get the declaration to access a member for a field.""" @@ -481,6 +491,26 @@ class _CppHeaderFileWriter(_CppFileWriterBase): self._writer.write_template( '${const_type}${param_type} ${method_name}() const { ${body} }') + def gen_validator(self, field): + # type: (ast.Field) -> None + """Generate the C++ validator definition for a field.""" + + template_params = { + 'method_name': _get_field_member_validator_name(field), + 'param_type': cpp_types.get_cpp_type(field).get_getter_setter_type() + } + + with self._with_template(template_params): + if field.validator is None: + # Header inline the Status::OK stub for non-validated fields. + self._writer.write_template( + 'Status ${method_name}(${param_type}) { return Status::OK(); }') + else: + # Declare method implemented in C++ file. + self._writer.write_template('Status ${method_name}(${param_type});') + + self._writer.write_empty_line() + def gen_setter(self, field): # type: (ast.Field) -> None """Generate the C++ setter definition for a field.""" @@ -492,17 +522,22 @@ class _CppHeaderFileWriter(_CppFileWriterBase): if _is_required_serializer_field(field): post_body = '%s = true;' % (_get_has_field_member_name(field)) + validator = '' + if field.validator is not None: + validator = 'uassertStatusOK(%s(value));' % _get_field_member_validator_name(field) + template_params = { 'method_name': _get_field_member_setter_name(field), 'member_name': member_name, 'param_type': param_type, 'body': cpp_type_info.get_setter_body(member_name), 'post_body': post_body, + 'validator': validator, } with self._with_template(template_params): self._writer.write_template( - 'void ${method_name}(${param_type} value) & ' + '{ ${body} ${post_body} }') + 'void ${method_name}(${param_type} value) & { ${validator} ${body} ${post_body} }') self._writer.write_empty_line() @@ -704,6 +739,7 @@ class _CppHeaderFileWriter(_CppFileWriterBase): self.gen_description_comment(field.description) self.gen_getter(struct, field) if not struct.immutable and not field.chained_struct_field: + self.gen_validator(field) self.gen_setter(field) if struct.generate_comparison_operators: @@ -868,6 +904,20 @@ class _CppSourceFileWriter(_CppFileWriterBase): self._gen_array_deserializer(field, bson_element) return + def validate_and_assign_or_uassert(field, expression): + # type: (ast.Field, unicode) -> None + """Perform field value validation post-assignment.""" + field_name = _get_field_member_name(field) + if field.validator is None: + self._writer.write_line('%s = %s;' % (field_name, expression)) + return + + with self._block('{', '}'): + self._writer.write_line('auto value = %s;' % (expression)) + self._writer.write_line('uassertStatusOK(%s(value));' % + (_get_field_member_validator_name(field))) + self._writer.write_line('%s = std::move(value);' % (field_name)) + if field.chained: # Do not generate a predicate check since we always call these deserializers. @@ -881,8 +931,8 @@ class _CppSourceFileWriter(_CppFileWriterBase): expression = "%s(%s)" % (method_name, bson_object) self._gen_usage_check(field, bson_element, field_usage_check) + validate_and_assign_or_uassert(field, expression) - self._writer.write_line('%s = %s;' % (_get_field_member_name(field), expression)) else: predicate = _get_bson_type_check(bson_element, 'ctxt', field) if predicate: @@ -893,12 +943,12 @@ class _CppSourceFileWriter(_CppFileWriterBase): object_value = self._gen_field_deserializer_expression(bson_element, field) if field.chained_struct_field: + # No need for explicit validation as setter will throw for us. self._writer.write_line('%s.%s(%s);' % (_get_field_member_name(field.chained_struct_field), _get_field_member_setter_name(field), object_value)) else: - self._writer.write_line('%s = %s;' % (_get_field_member_name(field), - object_value)) + validate_and_assign_or_uassert(field, object_value) def gen_doc_sequence_deserializer(self, field): # type: (ast.Field) -> None @@ -1132,6 +1182,53 @@ class _CppSourceFileWriter(_CppFileWriterBase): self._writer.write_line(method_info.get_call('object')) self._writer.write_line('return object;') + def gen_field_validators(self, struct): + # type: (ast.Struct) -> None + """Generate non-trivial field validators.""" + for field in struct.fields: + if field.validator is None: + # Fields without validators are implemented in the header. + continue + + cpp_type = cpp_types.get_cpp_type(field) + + method_template = { + 'class_name': common.title_case(struct.name), + 'method_name': _get_field_member_validator_name(field), + 'param_type': cpp_type.get_getter_setter_type(), + } + + def compare_and_return_status(op, limit): + # type: (unicode, Union[int, float]) -> None + """Emit a comparison which returns an BadValue Status on failure.""" + with self._block('if (!(value %s %s)) {' % (op, repr(limit)), '}'): + self._writer.write_line( + 'return {::mongo::ErrorCodes::BadValue, str::stream() << ' + + '"Value must be %s %s, \'" << value << "\' provided"};' % (op, limit)) + + validator = field.validator + with self._with_template(method_template): + self._writer.write_template( + 'Status ${class_name}::${method_name}(${param_type} value)') + with self._block('{', '}'): + if validator.gt is not None: + compare_and_return_status('>', validator.gt) + if validator.gte is not None: + compare_and_return_status('>=', validator.gte) + if validator.lt is not None: + compare_and_return_status('<', validator.lt) + if validator.lte is not None: + compare_and_return_status('<=', validator.lte) + + if validator.callback is not None: + with self._block('{', '}'): + self._writer.write_line('Status status = %s(value);' % + (validator.callback)) + with self._block('if (!status.isOK()) {', '}'): + self._writer.write_line('return status;') + + self._writer.write_line('return Status::OK();') + def gen_bson_deserializer_methods(self, struct): # type: (ast.Struct) -> None """Generate the C++ deserializer method definitions.""" @@ -1584,6 +1681,10 @@ class _CppSourceFileWriter(_CppFileWriterBase): self.gen_constructors(struct) self.write_empty_line() + # Write field validators + self.gen_field_validators(struct) + self.write_empty_line() + # Write deserializers self.gen_bson_deserializer_methods(struct) self.write_empty_line() diff --git a/buildscripts/idl/idl/parser.py b/buildscripts/idl/idl/parser.py index 052f9a21782..bb778a5633f 100644 --- a/buildscripts/idl/idl/parser.py +++ b/buildscripts/idl/idl/parser.py @@ -191,6 +191,23 @@ def _parse_type(ctxt, spec, name, node): spec.symbols.add_type(ctxt, idltype) +def _parse_validator(ctxt, node): + # type: (errors.ParserContext, yaml.nodes.MappingNode) -> syntax.Validator + """Parse a validator for a field.""" + validator = syntax.Validator(ctxt.file_name, node.start_mark.line, node.start_mark.column) + + _generic_parser( + ctxt, node, "validator", validator, { + "gt": _RuleDesc("scalar"), + "lt": _RuleDesc("scalar"), + "gte": _RuleDesc("scalar"), + "lte": _RuleDesc("scalar"), + "callback": _RuleDesc("scalar"), + }) + + return validator + + def _parse_field(ctxt, name, node): # type: (errors.ParserContext, str, Union[yaml.nodes.MappingNode, yaml.nodes.ScalarNode, yaml.nodes.SequenceNode]) -> syntax.Field """Parse a field in a struct/command in the IDL file.""" @@ -207,6 +224,7 @@ def _parse_field(ctxt, name, node): "default": _RuleDesc('scalar'), "supports_doc_sequence": _RuleDesc("bool_scalar"), "comparison_order": _RuleDesc("int_scalar"), + "validator": _RuleDesc('mapping', mapping_parser_func=_parse_validator), }) return field diff --git a/buildscripts/idl/idl/syntax.py b/buildscripts/idl/idl/syntax.py index 049114b5d9f..1f354b9768f 100644 --- a/buildscripts/idl/idl/syntax.py +++ b/buildscripts/idl/idl/syntax.py @@ -263,6 +263,30 @@ class Type(common.SourceLocation): super(Type, self).__init__(file_name, line, column) +class Validator(common.SourceLocation): + """ + An instance of a validator for a field. + + The validator must include at least one of the defined validation predicates. + If more than one is included, they must ALL evaluate to true. + """ + + # pylint: disable=too-many-instance-attributes + + def __init__(self, file_name, line, column): + # type: (unicode, int, int) -> None + """Construct a Validator.""" + # Don't lint gt/lt as bad attibute names. + # pylint: disable=C0103 + self.gt = None # type: unicode + self.lt = None # type: unicode + self.gte = None # type: unicode + self.lte = None # type: unicode + self.callback = None # type: unicode + + super(Validator, self).__init__(file_name, line, column) + + class Field(common.SourceLocation): """ An instance of a field in a struct. @@ -286,6 +310,7 @@ class Field(common.SourceLocation): self.default = None # type: unicode self.supports_doc_sequence = False # type: bool self.comparison_order = -1 # type: int + self.validator = None # type: Validator # Internal fields - not generated by parser self.serialize_op_msg_request_only = False # type: bool diff --git a/src/mongo/idl/idl_test.cpp b/src/mongo/idl/idl_test.cpp index ed5ea3dc6ab..341509638f5 100644 --- a/src/mongo/idl/idl_test.cpp +++ b/src/mongo/idl/idl_test.cpp @@ -2401,6 +2401,214 @@ TEST(IDLChainedStruct, TestInline) { } } +TEST(IDLValidatedField, Int_basic_ranges) { + // Explicitly call setters. + Int_basic_ranges obj0; + obj0.setPositive_int(42); + ASSERT_THROWS(obj0.setPositive_int(0), AssertionException); + ASSERT_THROWS(obj0.setPositive_int(-42), AssertionException); + + ASSERT_THROWS(obj0.setNegative_int(42), AssertionException); + ASSERT_THROWS(obj0.setNegative_int(0), AssertionException); + obj0.setNegative_int(-42); + + obj0.setNon_negative_int(42); + obj0.setNon_negative_int(0); + ASSERT_THROWS(obj0.setNon_negative_int(-42), AssertionException); + + ASSERT_THROWS(obj0.setNon_positive_int(42), AssertionException); + obj0.setNon_positive_int(0); + obj0.setNon_positive_int(-42); + + ASSERT_THROWS(obj0.setByte_range_int(-1), AssertionException); + obj0.setByte_range_int(0); + obj0.setByte_range_int(127); + obj0.setByte_range_int(128); + obj0.setByte_range_int(255); + ASSERT_THROWS(obj0.setByte_range_int(256), AssertionException); + + // IDL ints *are* int32_t, so no number we can pass to the func will actually fail. + obj0.setRange_int(-2147483648); + obj0.setRange_int(-65536); + obj0.setRange_int(0); + obj0.setRange_int(65536); + obj0.setRange_int(2147483647); + + // Positive case parsing. + const auto tryPass = [](std::int32_t pos, + std::int32_t neg, + std::int32_t nonneg, + std::int32_t nonpos, + std::int32_t byte_range, + std::int32_t int_range) { + IDLParserErrorContext ctxt("root"); + auto doc = + BSON("positive_int" << pos << "negative_int" << neg << "non_negative_int" << nonneg + << "non_positive_int" + << nonpos + << "byte_range_int" + << byte_range + << "range_int" + << int_range); + auto obj = Int_basic_ranges::parse(ctxt, doc); + ASSERT_EQUALS(obj.getPositive_int(), pos); + ASSERT_EQUALS(obj.getNegative_int(), neg); + ASSERT_EQUALS(obj.getNon_negative_int(), nonneg); + ASSERT_EQUALS(obj.getNon_positive_int(), nonpos); + ASSERT_EQUALS(obj.getByte_range_int(), byte_range); + ASSERT_EQUALS(obj.getRange_int(), int_range); + }; + + // Negative case parsing. + const auto tryFail = [](std::int32_t pos, + std::int32_t neg, + std::int32_t nonneg, + std::int32_t nonpos, + std::int32_t byte_range, + std::int32_t int_range) { + IDLParserErrorContext ctxt("root"); + auto doc = + BSON("positive_int" << pos << "negative_int" << neg << "non_negative_int" << nonneg + << "non_positive_int" + << nonpos + << "byte_range_int" + << byte_range + << "range_int" + << int_range); + ASSERT_THROWS(Int_basic_ranges::parse(ctxt, doc), AssertionException); + }; + + tryPass(1, -1, 0, 0, 128, 65537); + tryFail(0, -1, 0, 0, 128, 65537); + tryFail(1, 0, 0, 0, 128, 65537); + tryFail(1, -1, -1, 0, 128, 65537); + tryFail(1, -1, 0, 1, 128, 65537); + tryFail(1, -1, 0, 0, 256, 65537); + tryFail(0, 0, -1, 1, 257, 0); + + tryPass(1000, -1000, 1, -1, 127, 0x7FFFFFFF); +} + +TEST(IDLValidatedField, Double_basic_ranges) { + // Explicitly call setters. + Double_basic_ranges obj0; + obj0.setPositive_double(42.0); + obj0.setPositive_double(0.000000000001); + ASSERT_THROWS(obj0.setPositive_double(0.0), AssertionException); + ASSERT_THROWS(obj0.setPositive_double(-42.0), AssertionException); + + ASSERT_THROWS(obj0.setNegative_double(42.0), AssertionException); + ASSERT_THROWS(obj0.setNegative_double(0.0), AssertionException); + obj0.setNegative_double(-0.000000000001); + obj0.setNegative_double(-42.0); + + obj0.setNon_negative_double(42.0); + obj0.setNon_negative_double(0.0); + ASSERT_THROWS(obj0.setNon_negative_double(-42.0), AssertionException); + + ASSERT_THROWS(obj0.setNon_positive_double(42.0), AssertionException); + obj0.setNon_positive_double(0.0); + obj0.setNon_positive_double(-42.0); + + ASSERT_THROWS(obj0.setRange_double(-12345678901234600000.0), AssertionException); + obj0.setRange_double(-12345678901234500000.0); + obj0.setRange_double(-3000000000.0); + obj0.setRange_double(0); + obj0.setRange_double(3000000000); + obj0.setRange_double(12345678901234500000.0); + ASSERT_THROWS(obj0.setRange_double(12345678901234600000.0), AssertionException); + + // Positive case parsing. + const auto tryPass = + [](double pos, double neg, double nonneg, double nonpos, double double_range) { + IDLParserErrorContext ctxt("root"); + auto doc = + BSON("positive_double" << pos << "negative_double" << neg << "non_negative_double" + << nonneg + << "non_positive_double" + << nonpos + << "range_double" + << double_range); + auto obj = Double_basic_ranges::parse(ctxt, doc); + ASSERT_EQUALS(obj.getPositive_double(), pos); + ASSERT_EQUALS(obj.getNegative_double(), neg); + ASSERT_EQUALS(obj.getNon_negative_double(), nonneg); + ASSERT_EQUALS(obj.getNon_positive_double(), nonpos); + ASSERT_EQUALS(obj.getRange_double(), double_range); + }; + + // Negative case parsing. + const auto tryFail = + [](double pos, double neg, double nonneg, double nonpos, double double_range) { + IDLParserErrorContext ctxt("root"); + auto doc = + BSON("positive_double" << pos << "negative_double" << neg << "non_negative_double" + << nonneg + << "non_positive_double" + << nonpos + << "range_double" + << double_range); + ASSERT_THROWS(Double_basic_ranges::parse(ctxt, doc), AssertionException); + }; + + tryPass(1, -1, 0, 0, 123456789012345); + tryFail(0, -1, 0, 0, 123456789012345); + tryFail(1, 0, 0, 0, 123456789012345); + tryFail(1, -1, -1, 0, 123456789012345); + tryFail(1, -1, 0, 1, 123456789012345); + tryFail(1, -1, 0, -1, 12345678901234600000.0); + tryPass(0.00000000001, -0.00000000001, 0.0, 0.0, 1.23456789012345); +} + +TEST(IDLValidatedField, Callback_validators) { + // Explicitly call setters. + Callback_validators obj0; + obj0.setInt_even(42); + ASSERT_THROWS(obj0.setInt_even(7), AssertionException); + obj0.setInt_even(0); + ASSERT_THROWS(obj0.setInt_even(-7), AssertionException); + obj0.setInt_even(-42); + + ASSERT_THROWS(obj0.setDouble_nearly_int(3.141592), AssertionException); + ASSERT_THROWS(obj0.setDouble_nearly_int(-2.71828), AssertionException); + obj0.setDouble_nearly_int(0.0); + obj0.setDouble_nearly_int(1.0); + obj0.setDouble_nearly_int(1.05); + obj0.setDouble_nearly_int(-123456789.01234500000); + + ASSERT_THROWS(obj0.setString_starts_with_x("whiskey"), AssertionException); + obj0.setString_starts_with_x("x-ray"); + ASSERT_THROWS(obj0.setString_starts_with_x("yankee"), AssertionException); + + // Positive case parsing. + const auto tryPass = + [](std::int32_t int_even, double double_nearly_int, StringData string_starts_with_x) { + IDLParserErrorContext ctxt("root"); + auto doc = BSON("int_even" << int_even << "double_nearly_int" << double_nearly_int + << "string_starts_with_x" + << string_starts_with_x); + auto obj = Callback_validators::parse(ctxt, doc); + ASSERT_EQUALS(obj.getInt_even(), int_even); + ASSERT_EQUALS(obj.getDouble_nearly_int(), double_nearly_int); + ASSERT_EQUALS(obj.getString_starts_with_x(), string_starts_with_x); + }; + + // Negative case parsing. + const auto tryFail = + [](std::int32_t int_even, double double_nearly_int, StringData string_starts_with_x) { + IDLParserErrorContext ctxt("root"); + auto doc = BSON("int_even" << int_even << "double_nearly_int" << double_nearly_int + << "string_starts_with_x" + << string_starts_with_x); + ASSERT_THROWS(Callback_validators::parse(ctxt, doc), AssertionException); + }; + + tryPass(42, 123456789.01, "x-ray"); + tryFail(43, 123456789.01, "x-ray"); + tryFail(42, 123456789.11, "x-ray"); + tryFail(42, 123456789.01, "uniform"); +} + // Positive: verify a command a string arg TEST(IDLTypeCommand, TestString) { IDLParserErrorContext ctxt("root"); diff --git a/src/mongo/idl/idl_test.h b/src/mongo/idl/idl_test.h new file mode 100644 index 00000000000..ba9cbf7dfe6 --- /dev/null +++ b/src/mongo/idl/idl_test.h @@ -0,0 +1,70 @@ +/** + * Copyright (C) 2017 MongoDB Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + * As a special exception, the copyright holders give permission to link the + * code of portions of this program with the OpenSSL library under certain + * conditions as described in each individual source file and distribute + * linked combinations including the program with the OpenSSL library. You + * must comply with the GNU Affero General Public License in all respects + * for all of the code used other than as permitted herein. If you modify + * file(s) with this exception, you may extend this exception to your + * version of the file(s), but you are not obligated to do so. If you do not + * wish to do so, delete this exception statement from your version. If you + * delete this exception statement from all source files in the program, + * then also delete it in the license file. + */ + +#include "mongo/base/status.h" + +namespace mongo { +namespace idl { +namespace test { + +/** + * Validates the given number is even + */ +inline Status validateEvenNumber(std::int32_t value) { + if (value & 1) { + return {ErrorCodes::BadValue, "Value must be even"}; + } + return Status::OK(); +} + +/** + * Validates that the number presented is within 0.1 of an integer value + */ +inline Status validateNearlyInt(double value) { + value = fabs(value); + value = value - static_cast<std::uint64_t>(value); + if ((value > 0.1) && (value < 0.9)) { + return {ErrorCodes::BadValue, "Value is too far from being an integer"}; + } + return Status::OK(); +} + +/** + * Validates that the provided string starts with a given letter. + */ +template <char letter> +Status validateStartsWith(StringData value) { + if ((value.empty() || value[0] != letter)) { + return {ErrorCodes::BadValue, "Value does not begin with correct letter"}; + } + return Status::OK(); +} + +} // namespace test +} // namespace idl +} // namespace mongo diff --git a/src/mongo/idl/unittest.idl b/src/mongo/idl/unittest.idl index ddbe386fd91..56c175e095a 100644 --- a/src/mongo/idl/unittest.idl +++ b/src/mongo/idl/unittest.idl @@ -19,6 +19,7 @@ global: cpp_namespace: "mongo::idl::test" cpp_includes: - "mongo/idl/idl_test_types.h" + - "mongo/idl/idl_test.h" imports: - "mongo/idl/basic_types.idl" @@ -498,6 +499,67 @@ structs: ################################################################################################## # +# Using Validators +# +################################################################################################## + + int_basic_ranges: + description: Struct using basic range validators on ints + fields: + positive_int: + type: int + validator: { gt: 0 } + negative_int: + type: int + validator: { lt: 0 } + non_negative_int: + type: int + validator: { gte: 0 } + non_positive_int: + type: int + validator: { lte: 0 } + byte_range_int: + type: int + validator: { gte: 0, lt: 256 } + range_int: + type: int + validator: { gte: -2147483658, lte: 2147483647 } + + double_basic_ranges: + description: Struct using basic range validators on doubles + fields: + positive_double: + type: double + validator: { gt: 0.0 } + negative_double: + type: double + validator: { lt: 0.0 } + non_negative_double: + type: double + validator: { gte: 0.0 } + non_positive_double: + type: double + validator: { lte: 0.0 } + range_double: + type: double + validator: { gte: -12345678901234500000, lte: 12345678901234500000 } + + callback_validators: + description: Struct using fields with callback validators + fields: + int_even: + type: int + validator: { callback: 'validateEvenNumber' } + double_nearly_int: + type: double + validator: { callback: 'validateNearlyInt' } + string_starts_with_x: + type: string + validator: { callback: "validateStartsWith<'x'>" } + + +################################################################################################## +# # Test commands # ################################################################################################## @@ -618,4 +680,3 @@ commands: fields: field1: int - |