def tokenize_json(content): assert isinstance(content, (str, bytes)) if isinstance(content, bytes): content = content.decode('utf-8', 'ignore') if not content.strip(): message = ErrorMessage(text='No content.', code='parse_error', position=Position(line_no=1, column_no=1, index=0)) raise ParseError(messages=[message], summary='Invalid JSON.') try: decoder = _TokenizingDecoder(content=content) return decoder.decode(content) except json.decoder.JSONDecodeError as exc: message = ErrorMessage( text=_strip_endings(exc.msg, [" starting at", " at"]) + ".", code='parse_error', position=Position(line_no=exc.lineno, column_no=exc.colno, index=exc.pos)) raise ParseError(messages=[message], summary='Invalid JSON.') from None
def test_invalid_properties(): with pytest.raises(ValidationError) as exc: parse_json('{"a": "abc", "b": 123}', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ ErrorMessage('Must be a number.', Marker(6)), ErrorMessage('Invalid property name.', Marker(13)) ] assert error_messages == expecting
def test_invalid_top_level_item(): with pytest.raises(ValidationError) as exc: parse_json('123', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ErrorMessage('Must be an object.', Marker(0))] assert error_messages == expecting
def test_object_unterminated_after_value(): with pytest.raises(ParseError) as exc: parse_json('{"abc": "def"', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ErrorMessage("Expecting ',' delimiter.", Marker(13))] assert error_messages == expecting
def test_object_unterminated_after_key(): with pytest.raises(ParseError) as exc: parse_json('{"abc": ', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ErrorMessage("Expecting value.", Marker(8))] assert error_messages == expecting
def test_invalid_token(): with pytest.raises(ParseError) as exc: parse_json('-', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ErrorMessage("Expecting value.", Marker(0))] assert error_messages == expecting
def test_object_missing_comma_delimiter(): with pytest.raises(ParseError) as exc: parse_json('{"abc": "def" 1', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ErrorMessage("Expecting ',' delimiter.", Marker(14))] assert error_messages == expecting
def test_invalid_property(): with pytest.raises(ValidationError) as exc: parse_json('{"a": "abc"}', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ErrorMessage("Must be a number.", Marker(6))] assert error_messages == expecting
def test_empty_string(): with pytest.raises(ParseError) as exc: parse_json(b'', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ErrorMessage('No content.', Marker(0))] assert error_messages == expecting
def test_invalid_properties(): with pytest.raises(ValidationError) as exc: apistar.parse('{"a": "abc", "b": 123}', encoding="json", validator=VALIDATOR) assert exc.value.messages == [ ErrorMessage( text='Must be a number.', code='type', index=['a'], position=Position(line_no=1, column_no=7, index=6) ), ErrorMessage( text='Invalid property name.', code='invalid_property', index=['b'], position=Position(line_no=1, column_no=14, index=13) ) ]
def test_object_invalid_property_name(): with pytest.raises(ParseError) as exc: parse_json('{"abc": "def", 1', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ ErrorMessage("Expecting property name enclosed in double quotes.", Marker(15)) ] assert error_messages == expecting
def decode(self, bytestring, **options): try: data = json.loads(bytestring.decode('utf-8'), object_pairs_hook=dict_type) except ValueError as exc: message = ErrorMessage(text='Malformed JSON. %s' % exc, code='parse_failed') raise ParseError(messages=[message]) from None jsonschema = JSON_SCHEMA.validate(data) return decode(jsonschema)
def test_object_missing_property_name(): with pytest.raises(ParseError) as exc: parse_json('{', VALIDATOR) error_messages = exc.value.get_error_messages() expecting = [ ErrorMessage('Expecting property name enclosed in double quotes.', Marker(1)) ] assert error_messages == expecting
def test_unterminated_string(): with pytest.raises(ParseError) as exc: apistar.parse('"ab', encoding='json') assert exc.value.messages == [ ErrorMessage( text="Unterminated string.", code='parse_error', position=Position(line_no=1, column_no=1, index=0) ) ]
def test_object_invalid_property_name(): with pytest.raises(ParseError) as exc: apistar.parse('{"abc": "def", 1', encoding='json') assert exc.value.messages == [ ErrorMessage( text="Expecting property name enclosed in double quotes.", code='parse_error', position=Position(line_no=1, column_no=16, index=15) ) ]
def test_object_missing_comma_delimiter(): with pytest.raises(ParseError) as exc: apistar.parse('{"abc": "def" 1', encoding='json') assert exc.value.messages == [ ErrorMessage( text="Expecting ',' delimiter.", code='parse_error', position=Position(line_no=1, column_no=15, index=14) ) ]
def test_object_missing_property_name(): with pytest.raises(ParseError) as exc: apistar.parse('{', encoding='json') assert exc.value.messages == [ ErrorMessage( text='Expecting property name enclosed in double quotes.', code='parse_error', position=Position(line_no=1, column_no=2, index=1) ) ]
def test_empty_string(): with pytest.raises(ParseError) as exc: apistar.parse(b'', encoding='json') assert exc.value.messages == [ ErrorMessage( text='No content.', code='parse_error', position=Position(line_no=1, column_no=1, index=0) ) ]
def test_missing_required_property(): with pytest.raises(ValidationError) as exc: apistar.parse('{}', encoding="json", validator=VALIDATOR) assert exc.value.messages == [ ErrorMessage( text='The "a" field is required.', code='required', index=['a'], position=Position(line_no=1, column_no=1, index=0)) ]
def test_object_unterminated_after_value(): with pytest.raises(ParseError) as exc: apistar.parse('{"abc": "def"', encoding='json') assert exc.value.messages == [ ErrorMessage( text="Expecting ',' delimiter.", code='parse_error', position=Position(line_no=1, column_no=14, index=13) ) ]
def test_invalid_token(): with pytest.raises(ParseError) as exc: apistar.parse('-', encoding='json') assert exc.value.messages == [ ErrorMessage( text="Expecting value.", code='parse_error', position=Position(line_no=1, column_no=1, index=0) ) ]
def test_invalid_top_level_item(): with pytest.raises(ValidationError) as exc: apistar.parse('123', encoding="json", validator=VALIDATOR) assert exc.value.messages == [ ErrorMessage( text='Must be an object.', code='type', index=None, position=Position(line_no=1, column_no=1, index=0) ) ]
def parse(content, encoding=None, validator=None): if encoding not in (None, "json", "yaml"): raise ValueError('encoding must be either "json" or "yaml"') if encoding is None: if INFER_YAML.match(content): encoding = "yaml" elif INFER_JSON.match(content): encoding = "json" else: message = ErrorMessage( text= "Unable to guess if encoding is JSON or YAML. Use the 'encoding' argument.", code="unknown_encoding") raise ValidationError(messages=[message]) if encoding == "json": token = tokenize_json(content) else: token = tokenize_yaml(content) value = token.get_value() if validator is not None: try: value = validator.validate(value) except ValidationError as exc: for message in exc.messages: if message.code == 'required': message.position = token.lookup_position( message.index[:-1]) elif message.code in ['invalid_property', 'invalid_key']: message.position = token.lookup_key_position(message.index) else: message.position = token.lookup_position(message.index) exc.messages = sorted(exc.messages, key=lambda x: x.position.index) raise exc return (value, token)
def tokenize_yaml(content): class CustomLoader(SafeLoader): pass def construct_mapping(loader, node): start = node.start_mark.index end = node.end_mark.index mapping = loader.construct_mapping(node) return DictToken(mapping, start, end - 1, content=content) def construct_sequence(loader, node): start = node.start_mark.index end = node.end_mark.index value = loader.construct_sequence(node) return ListToken(value, start, end - 1, content=content) def construct_scalar(loader, node): start = node.start_mark.index end = node.end_mark.index value = loader.construct_scalar(node) return ScalarToken(value, start, end - 1, content=content) def construct_int(loader, node): start = node.start_mark.index end = node.end_mark.index value = loader.construct_yaml_int(node) return ScalarToken(value, start, end - 1, content=content) def construct_float(loader, node): start = node.start_mark.index end = node.end_mark.index value = loader.construct_yaml_float(node) return ScalarToken(value, start, end - 1, content=content) def construct_bool(loader, node): start = node.start_mark.index end = node.end_mark.index value = loader.construct_yaml_bool(node) return ScalarToken(value, start, end - 1, content=content) def construct_null(loader, node): start = node.start_mark.index end = node.end_mark.index value = loader.construct_yaml_null(node) return ScalarToken(value, start, end - 1, content=content) CustomLoader.add_constructor( yaml.resolver.BaseResolver.DEFAULT_MAPPING_TAG, construct_mapping) CustomLoader.add_constructor( yaml.resolver.BaseResolver.DEFAULT_SEQUENCE_TAG, construct_sequence) CustomLoader.add_constructor(yaml.resolver.BaseResolver.DEFAULT_SCALAR_TAG, construct_scalar) CustomLoader.add_constructor('tag:yaml.org,2002:int', construct_int) CustomLoader.add_constructor('tag:yaml.org,2002:float', construct_float) CustomLoader.add_constructor('tag:yaml.org,2002:bool', construct_bool) CustomLoader.add_constructor('tag:yaml.org,2002:null', construct_null) assert isinstance(content, (str, bytes)) if isinstance(content, bytes): content = content.decode('utf-8', 'ignore') if not content.strip(): message = ErrorMessage(text='No content.', code='parse_error', position=Position(line_no=1, column_no=1, index=0)) raise ParseError(errors=[message], summary='Invalid YAML.') try: return yaml.load(content, CustomLoader) except (yaml.scanner.ScannerError, yaml.parser.ParserError) as exc: index = getattr(exc, 'index', 0) message = ErrorMessage(text=exc.problem + ".", code='parse_error', position=_get_position(content, index=index)) raise ParseError(messages=[message], summary='Invalid YAML.') from None
def validate(schema, format=None, encoding=None): if format not in FORMAT_CHOICES: raise ValueError('format must be one of %s' % FORMAT_CHOICES) if isinstance(schema, (str, bytes)): value, token = parse(schema, encoding) elif isinstance(schema, dict): if encoding is not None: raise ValueError('encoding must be `None`.') value, token = schema, None else: raise ValueError( 'schema must either be a dict, or a string/bytestring.') if format is None: if isinstance(value, dict) and "openapi" in value and "swagger" not in value: format = "openapi" elif isinstance( value, dict) and "swagger" in value and "openapi" not in value: format = "swagger" else: message = ErrorMessage( text= "Unable to determine schema format. Use the 'format' argument.", code='unknown_format') raise ValidationError(messages=[message]) validator = { 'config': APISTAR_CONFIG, 'jsonschema': JSON_SCHEMA, 'openapi': OPEN_API, 'swagger': SWAGGER }[format] if validator is not None: try: value = validator.validate(value) except ValidationError as exc: exc.summary = { 'config': 'Invalid configuration file.', 'jsonschema': 'Invalid JSONSchema document.', 'openapi': 'Invalid OpenAPI schema.', 'swagger': 'Invalid Swagger schema.', }[format] if token is not None: for message in exc.messages: if message.code == 'required': message.position = token.lookup_position( message.index[:-1]) elif message.code in ['invalid_property', 'invalid_key']: message.position = token.lookup_key_position( message.index) else: message.position = token.lookup_position(message.index) exc.messages = sorted(exc.messages, key=lambda x: x.position.index) raise exc if format in ['openapi', 'swagger']: decoder = {'openapi': OpenAPI, 'swagger': Swagger}[format] value = decoder().load(value) return value
def validate(self, value, definitions=None, allow_coerce=False): if value is None and self.allow_null: return None elif value is None: self.error('null') elif not isinstance(value, list): self.error('type') definitions = self.get_definitions(definitions) validated = [] if self.min_items is not None and self.min_items == self.max_items and len(value) != self.min_items: self.error('exact_items') if self.min_items is not None and len(value) < self.min_items: if self.min_items == 1: self.error('empty') self.error('min_items') elif self.max_items is not None and len(value) > self.max_items: self.error('max_items') elif isinstance(self.items, list) and (self.additional_items is False) and len(value) > len(self.items): self.error('additional_items') # Ensure all items are of the right type. errors = {} if self.unique_items: seen_items = Uniqueness() for pos, item in enumerate(value): try: if isinstance(self.items, list): if pos < len(self.items): item = self.items[pos].validate( item, definitions=definitions, allow_coerce=allow_coerce ) elif isinstance(self.additional_items, Validator): item = self.additional_items.validate( item, definitions=definitions, allow_coerce=allow_coerce ) elif self.items is not None: item = self.items.validate( item, definitions=definitions, allow_coerce=allow_coerce ) if self.unique_items: if item in seen_items: self.error('unique_items') else: seen_items.add(item) validated.append(item) except ValidationError as exc: errors[pos] = exc.messages if errors: error_messages = [] for key, messages in errors.items(): for message in messages: index = [key] if message.index is None else [key] + message.index error_message = ErrorMessage(message.text, message.code, index) error_messages.append(error_message) raise ValidationError(error_messages) return validated
def validate(self, value, definitions=None, allow_coerce=False): if value is None and self.allow_null: return None elif value is None: self.error('null') elif not isinstance(value, (dict, typing.Mapping)): self.error('type') definitions = self.get_definitions(definitions) validated = dict_type() # Ensure all property keys are strings. errors = {} for key in value.keys(): if not isinstance(key, str): errors[key] = [self.error_message('invalid_key')] # Min/Max properties if self.min_properties is not None: if len(value) < self.min_properties: if self.min_properties == 1: self.error('empty') else: self.error('min_properties') if self.max_properties is not None: if len(value) > self.max_properties: self.error('max_properties') # Required properties for key in self.required: if key not in value: errors[key] = [self.error_message('required', field_name=key)] # Properties for key, child_schema in self.properties.items(): if key not in value: if child_schema.has_default(): validated[key] = child_schema.default continue item = value[key] try: validated[key] = child_schema.validate( item, definitions=definitions, allow_coerce=allow_coerce ) except ValidationError as exc: errors[key] = exc.messages # Pattern properties if self.pattern_properties: for key in list(value.keys()): for pattern, child_schema in self.pattern_properties.items(): if isinstance(key, str) and re.search(pattern, key): item = value[key] try: validated[key] = child_schema.validate( item, definitions=definitions, allow_coerce=allow_coerce ) except ValidationError as exc: errors[key] = exc.messages # Additional properties remaining = [ key for key in value.keys() if key not in set(validated.keys()) | set(errors.keys()) ] if self.additional_properties is True: for key in remaining: validated[key] = value[key] elif self.additional_properties is False: for key in remaining: errors[key] = [self.error_message('invalid_property')] elif self.additional_properties is not None: child_schema = self.additional_properties for key in remaining: item = value[key] try: validated[key] = child_schema.validate( item, definitions=definitions, allow_coerce=allow_coerce ) except ValidationError as exc: errors[key] = exc.messages if errors: error_messages = [] for key, messages in errors.items(): for message in messages: index = [key] if message.index is None else [key] + message.index error_message = ErrorMessage(message.text, message.code, index) error_messages.append(error_message) raise ValidationError(error_messages) return validated
def error_message(self, code, **context): text = self.errors[code].format(**self.__dict__, **context) return ErrorMessage(text=text, code=code)