Browse Source

hook up and test replacing of validate w/jsonschema

jsonschema
Ross McFarland 6 months ago
parent
commit
e26e3be59d
No known key found for this signature in database GPG Key ID: 943B179E15D3B22A
3 changed files with 99 additions and 79 deletions
  1. +70
    -49
      octodns/record/base.py
  2. +13
    -11
      tests/test_octodns_record_a.py
  3. +16
    -19
      tests/test_octodns_record_aaaa.py

+ 70
- 49
octodns/record/base.py View File

@ -6,6 +6,8 @@ from collections import defaultdict
from copy import deepcopy
from logging import getLogger
from jsonschema import Draft202012Validator
from ..context import ContextDict
from ..deprecation import deprecated
from ..equality import EqualityTupleMixin
@ -24,6 +26,7 @@ class Record(EqualityTupleMixin):
log = getLogger('Record')
_CLASSES = {}
_SCHEMAS = {}
@classmethod
def register_type(cls, _class, _type=None):
@ -37,58 +40,18 @@ class Record(EqualityTupleMixin):
raise RecordException(msg)
cls._CLASSES[_type] = _class
try:
cls._SCHEMAS[_type] = Draft202012Validator(
schema=_class.jsonschema(),
format_checker=Draft202012Validator.FORMAT_CHECKER,
)
except AttributeError:
pass
@classmethod
def registered_types(cls):
return cls._CLASSES
@classmethod
def jsonschema(cls):
schema = {
# base Record requirements
'title': 'Record',
'type': 'object',
'properties': {
'type': {'type': 'string', 'enum': list(cls._CLASSES.keys())}
},
}
class_schemas = []
for _type, _class in cls._CLASSES.items():
_value_type = _class._value_type
if not hasattr(_value_type, 'jsonschema'):
# type does not support schema
continue
class_schemas.append(
{
'if': {'properties': {'type': {'enum': [_type]}}},
'then': {
'title': _type,
'properties': {
'type': {},
'ttl': {
'type': 'integer',
'minimum': 0,
'maximum': 86400,
},
'value': _class._value_type.jsonschema(),
},
'required': ['ttl', 'type', 'value'],
"unevaluatedProperties": False,
},
}
)
if class_schemas:
schema['allOf'] = class_schemas
# validate(schema=schema, instance={
# 'type': 'A',
# 'ttl': 42,
# 'value': 'nope',
# }, format_checker=Draft202012Validator.FORMAT_CHECKER)
return schema
@classmethod
def new(cls, zone, name, data, source=None, lenient=False):
reasons = []
@ -118,7 +81,20 @@ class Record(EqualityTupleMixin):
if context:
msg += f', {context}'
raise Exception(msg)
reasons.extend(_class.validate(name, fqdn, data))
validator = cls._SCHEMAS.get(_type)
if validator:
# new jsonschema based validaton
for e in validator.iter_errors(data):
message = e.message
for frm, to in e.schema.get('translations', {}).items():
if frm in message:
message = to
reasons.append(message)
else:
# original .validate
reasons.extend(_class.validate(name, fqdn, data))
try:
lenient |= data['octodns']['lenient']
except KeyError:
@ -347,6 +323,32 @@ class Record(EqualityTupleMixin):
class ValuesMixin(object):
@classmethod
def jsonschema(cls):
value_type = cls._value_type
schema = (
value_type.jsonschema() if hasattr(value_type, 'jsonschema') else {}
)
return {
'title': cls._type,
'properties': {
# TODO: what schema validations make sense for octodns?
'octodns': {},
'type': {'const': cls._type},
'ttl': {'type': 'integer', 'minimum': 0, 'maximum': 86400},
'value': schema,
'values': {'type': 'array', 'items': schema, 'minItems': 1},
},
'required': ['ttl', 'type'],
'oneOf': [{'required': ['value']}, {'required': ['values']}],
'translations': {
'is not valid under any of the given schemas': 'missing value(s)'
},
"unevaluatedProperties": False,
}
@classmethod
def validate(cls, name, fqdn, data):
reasons = super().validate(name, fqdn, data)
@ -416,6 +418,25 @@ class ValuesMixin(object):
class ValueMixin(object):
@classmethod
def jsonschema(cls):
value_type = cls._value_type
schema = (
value_type.jsonschema() if hasattr(value_type, 'jsonattr') else {}
)
return {
'title': cls._type,
'properties': {
'type': {},
'ttl': {'type': 'integer', 'minimum': 0, 'maximum': 86400},
'value': schema,
},
'required': ['ttl', 'type', 'value'],
"unevaluatedProperties": False,
}
@classmethod
def validate(cls, name, fqdn, data):
reasons = super().validate(name, fqdn, data)


+ 13
- 11
tests/test_octodns_record_a.py View File

@ -108,14 +108,14 @@ class TestRecordA(TestCase):
Record.new(
self.zone, 'www', {'type': 'A', 'ttl': 600, 'values': []}
)
self.assertEqual(['missing value(s)'], ctx.exception.reasons)
self.assertEqual(['[] should be non-empty'], ctx.exception.reasons)
# missing value(s), None values
with self.assertRaises(ValidationError) as ctx:
Record.new(
self.zone, 'www', {'type': 'A', 'ttl': 600, 'values': None}
)
self.assertEqual(['missing value(s)'], ctx.exception.reasons)
self.assertEqual(["None is not of type 'array'"], ctx.exception.reasons)
# missing value(s) and empty value
with self.assertRaises(ValidationError) as ctx:
@ -125,7 +125,8 @@ class TestRecordA(TestCase):
{'type': 'A', 'ttl': 600, 'values': [None, '']},
)
self.assertEqual(
['missing value(s)', 'empty value'], ctx.exception.reasons
["None is not of type 'string'", "'' is not a 'ipv4'"],
ctx.exception.reasons,
)
# missing value(s), None value
@ -133,18 +134,21 @@ class TestRecordA(TestCase):
Record.new(
self.zone, 'www', {'type': 'A', 'ttl': 600, 'value': None}
)
self.assertEqual(['missing value(s)'], ctx.exception.reasons)
self.assertEqual(
["None is not of type 'string'"], ctx.exception.reasons
)
# empty value, empty string value
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, 'www', {'type': 'A', 'ttl': 600, 'value': ''})
self.assertEqual(['empty value'], ctx.exception.reasons)
self.assertEqual(["'' is not a 'ipv4'"], ctx.exception.reasons)
# missing value(s) & ttl
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {'type': 'A'})
self.assertEqual(
['missing ttl', 'missing value(s)'], ctx.exception.reasons
["'ttl' is a required property", 'missing value(s)'],
ctx.exception.reasons,
)
# invalid ipv4 address
@ -152,9 +156,7 @@ class TestRecordA(TestCase):
Record.new(
self.zone, '', {'type': 'A', 'ttl': 600, 'value': 'hello'}
)
self.assertEqual(
['invalid IPv4 address "hello"'], ctx.exception.reasons
)
self.assertEqual(["'hello' is not a 'ipv4'"], ctx.exception.reasons)
# invalid ipv4 addresses
with self.assertRaises(ValidationError) as ctx:
@ -164,7 +166,7 @@ class TestRecordA(TestCase):
{'type': 'A', 'ttl': 600, 'values': ['hello', 'goodbye']},
)
self.assertEqual(
['invalid IPv4 address "hello"', 'invalid IPv4 address "goodbye"'],
["'hello' is not a 'ipv4'", "'goodbye' is not a 'ipv4'"],
ctx.exception.reasons,
)
@ -176,6 +178,6 @@ class TestRecordA(TestCase):
{'type': 'A', 'values': ['1.2.3.4', 'hello', '5.6.7.8']},
)
self.assertEqual(
['missing ttl', 'invalid IPv4 address "hello"'],
["'hello' is not a 'ipv4'", "'ttl' is a required property"],
ctx.exception.reasons,
)

+ 16
- 19
tests/test_octodns_record_aaaa.py View File

@ -87,14 +87,14 @@ class TestRecordAaaa(TestCase):
Record.new(
self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'values': []}
)
self.assertEqual(['missing value(s)'], ctx.exception.reasons)
self.assertEqual(['[] should be non-empty'], ctx.exception.reasons)
# missing value(s), None values
with self.assertRaises(ValidationError) as ctx:
Record.new(
self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'values': None}
)
self.assertEqual(['missing value(s)'], ctx.exception.reasons)
self.assertEqual(["None is not of type 'array'"], ctx.exception.reasons)
# missing value(s) and empty value
with self.assertRaises(ValidationError) as ctx:
@ -104,7 +104,8 @@ class TestRecordAaaa(TestCase):
{'type': 'AAAA', 'ttl': 600, 'values': [None, '']},
)
self.assertEqual(
['missing value(s)', 'empty value'], ctx.exception.reasons
["None is not of type 'string'", "'' is not a 'ipv6'"],
ctx.exception.reasons,
)
# missing value(s), None value
@ -112,20 +113,23 @@ class TestRecordAaaa(TestCase):
Record.new(
self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'value': None}
)
self.assertEqual(['missing value(s)'], ctx.exception.reasons)
self.assertEqual(
["None is not of type 'string'"], ctx.exception.reasons
)
# empty value, empty string value
with self.assertRaises(ValidationError) as ctx:
Record.new(
self.zone, 'www', {'type': 'AAAA', 'ttl': 600, 'value': ''}
)
self.assertEqual(['empty value'], ctx.exception.reasons)
self.assertEqual(["'' is not a 'ipv6'"], ctx.exception.reasons)
# missing value(s) & ttl
with self.assertRaises(ValidationError) as ctx:
Record.new(self.zone, '', {'type': 'AAAA'})
self.assertEqual(
['missing ttl', 'missing value(s)'], ctx.exception.reasons
["'ttl' is a required property", 'missing value(s)'],
ctx.exception.reasons,
)
# invalid IPv6 address
@ -133,9 +137,7 @@ class TestRecordAaaa(TestCase):
Record.new(
self.zone, '', {'type': 'AAAA', 'ttl': 600, 'value': 'hello'}
)
self.assertEqual(
['invalid IPv6 address "hello"'], ctx.exception.reasons
)
self.assertEqual(["'hello' is not a 'ipv6'"], ctx.exception.reasons)
# invalid IPv6 addresses
with self.assertRaises(ValidationError) as ctx:
@ -145,7 +147,7 @@ class TestRecordAaaa(TestCase):
{'type': 'AAAA', 'ttl': 600, 'values': ['hello', 'goodbye']},
)
self.assertEqual(
['invalid IPv6 address "hello"', 'invalid IPv6 address "goodbye"'],
["'hello' is not a 'ipv6'", "'goodbye' is not a 'ipv6'"],
ctx.exception.reasons,
)
@ -164,7 +166,7 @@ class TestRecordAaaa(TestCase):
},
)
self.assertEqual(
['missing ttl', 'invalid IPv6 address "hello"'],
["'hello' is not a 'ipv6'", "'ttl' is a required property"],
ctx.exception.reasons,
)
@ -197,9 +199,7 @@ class TestRecordAaaa(TestCase):
Record.new(
self.zone, '', {'type': 'AAAA', 'ttl': 600, 'value': 'hello'}
)
self.assertEqual(
['invalid IPv6 address "hello"'], ctx.exception.reasons
)
self.assertEqual(["'hello' is not a 'ipv6'"], ctx.exception.reasons)
with self.assertRaises(ValidationError) as ctx:
Record.new(
self.zone,
@ -207,10 +207,7 @@ class TestRecordAaaa(TestCase):
{'type': 'AAAA', 'ttl': 600, 'values': ['1.2.3.4', '2.3.4.5']},
)
self.assertEqual(
[
'invalid IPv6 address "1.2.3.4"',
'invalid IPv6 address "2.3.4.5"',
],
["'1.2.3.4' is not a 'ipv6'", "'2.3.4.5' is not a 'ipv6'"],
ctx.exception.reasons,
)
@ -222,6 +219,6 @@ class TestRecordAaaa(TestCase):
{'type': 'AAAA', 'ttl': 600, 'values': ['hello', 'goodbye']},
)
self.assertEqual(
['invalid IPv6 address "hello"', 'invalid IPv6 address "goodbye"'],
["'hello' is not a 'ipv6'", "'goodbye' is not a 'ipv6'"],
ctx.exception.reasons,
)

Loading…
Cancel
Save