1
0

schemas.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  1. from collections import namedtuple
  2. from coreschema.compat import text_types, numeric_types
  3. from coreschema.formats import validate_format
  4. from coreschema.utils import uniq
  5. import re
  6. Error = namedtuple('Error', ['text', 'index'])
  7. def push_index(errors, key):
  8. return [
  9. Error(error.text, [key] + error.index)
  10. for error in errors
  11. ]
  12. # TODO: Properties as OrderedDict if from list of tuples.
  13. # TODO: null keyword / Nullable
  14. # TODO: dependancies
  15. # TODO: remote ref
  16. # TODO: remaining formats
  17. # LATER: Enum display values
  18. # LATER: File
  19. # LATER: strict, coerce float etc...
  20. # LATER: decimals
  21. # LATER: override errors
  22. class Schema(object):
  23. errors = {}
  24. def __init__(self, title='', description='', default=None):
  25. self.title = title
  26. self.description = description
  27. self.default = default
  28. def make_error(self, code):
  29. error_string = self.errors[code]
  30. params = self.__dict__
  31. return Error(error_string.format(**params), [])
  32. def __or__(self, other):
  33. if isinstance(self, Union):
  34. self_children = self.children
  35. else:
  36. self_children = [self]
  37. if isinstance(other, Union):
  38. other_children = other.children
  39. else:
  40. other_children = [other]
  41. return Union(self_children + other_children)
  42. def __and__(self, other):
  43. if isinstance(self, Intersection):
  44. self_children = self.children
  45. else:
  46. self_children = [self]
  47. if isinstance(other, Intersection):
  48. other_children = other.children
  49. else:
  50. other_children = [other]
  51. return Intersection(self_children + other_children)
  52. def __xor__(self, other):
  53. return ExclusiveUnion([self, other])
  54. def __invert__(self):
  55. return Not(self)
  56. def __eq__(self, other):
  57. return (
  58. self.__class__ == other.__class__ and
  59. self.__dict__ == other.__dict__
  60. )
  61. class Object(Schema):
  62. errors = {
  63. 'type': 'Must be an object.',
  64. 'invalid_key': 'Object keys must be strings.',
  65. 'empty': 'Must not be empty.',
  66. 'required': 'This field is required.',
  67. 'max_properties': 'Must have no more than {max_properties} properties.',
  68. 'min_properties': 'Must have at least {min_properties} properties.',
  69. 'invalid_property': 'Invalid property.'
  70. }
  71. def __init__(self, properties=None, required=None, max_properties=None, min_properties=None, pattern_properties=None, additional_properties=True, **kwargs):
  72. super(Object, self).__init__(**kwargs)
  73. if isinstance(additional_properties, bool):
  74. # Handle `additional_properties` set to a boolean.
  75. self.additional_properties_schema = Anything()
  76. else:
  77. # Handle `additional_properties` set to a schema.
  78. self.additional_properties_schema = additional_properties
  79. additional_properties = True
  80. self.properties = properties
  81. self.required = required or []
  82. self.max_properties = max_properties
  83. self.min_properties = min_properties
  84. self.pattern_properties = pattern_properties
  85. self.additional_properties = additional_properties
  86. # Compile pattern regexes.
  87. self.pattern_properties_regex = None
  88. if pattern_properties is not None:
  89. self.pattern_properties_regex = {
  90. re.compile(key): value
  91. for key, value
  92. in pattern_properties.items()
  93. }
  94. def validate(self, value, context=None):
  95. if not isinstance(value, dict):
  96. return [self.make_error('type')]
  97. errors = []
  98. if any(not isinstance(key, text_types) for key in value.keys()):
  99. errors += [self.make_error('invalid_key')]
  100. if self.required is not None:
  101. for key in self.required:
  102. if key not in value:
  103. error_items = [self.make_error('required')]
  104. errors += push_index(error_items, key)
  105. if self.min_properties is not None:
  106. if len(value) < self.min_properties:
  107. if self.min_properties == 1:
  108. errors += [self.make_error('empty')]
  109. else:
  110. errors += [self.make_error('min_properties')]
  111. if self.max_properties is not None:
  112. if len(value) > self.max_properties:
  113. errors += [self.make_error('max_properties')]
  114. # Properties
  115. remaining_keys = set(value.keys())
  116. if self.properties is not None:
  117. remaining_keys -= set(self.properties.keys())
  118. for key, property_item in self.properties.items():
  119. if key not in value:
  120. continue
  121. error_items = property_item.validate(value[key], context)
  122. errors += push_index(error_items, key)
  123. # Pattern properties
  124. if self.pattern_properties is not None:
  125. for key in list(remaining_keys):
  126. for pattern, schema in self.pattern_properties_regex.items():
  127. if re.search(pattern, key):
  128. error_items = schema.validate(value[key], context)
  129. errors += push_index(error_items, key)
  130. remaining_keys.discard(key)
  131. # Additional properties
  132. if self.additional_properties:
  133. for key in remaining_keys:
  134. error_items = self.additional_properties_schema.validate(value[key], context)
  135. errors += push_index(error_items, key)
  136. else:
  137. for key in remaining_keys:
  138. error_items = [self.make_error('invalid_property')]
  139. errors += push_index(error_items, key)
  140. return errors
  141. class Array(Schema):
  142. errors = {
  143. 'type': 'Must be an array.',
  144. 'empty': 'Must not be empty.',
  145. 'max_items': 'Must have no more than {max_items} items.',
  146. 'min_items': 'Must have at least {min_items} items.',
  147. 'unique': 'Must not contain duplicate items.'
  148. }
  149. def __init__(self, items=None, max_items=None, min_items=None, unique_items=False, additional_items=True, **kwargs):
  150. super(Array, self).__init__(**kwargs)
  151. if items is None:
  152. items = Anything()
  153. if isinstance(items, list) and additional_items is False:
  154. # Setting additional_items==False implies a value for max_items.
  155. if max_items is None or max_items > len(items):
  156. max_items = len(items)
  157. self.items = items
  158. self.max_items = max_items
  159. self.min_items = min_items
  160. self.unique_items = unique_items
  161. self.additional_items = additional_items
  162. def validate(self, value, context=None):
  163. if not isinstance(value, list):
  164. return [self.make_error('type')]
  165. errors = []
  166. if self.items is not None:
  167. child_schema = self.items
  168. is_list = isinstance(self.items, list)
  169. for idx, item in enumerate(value):
  170. if is_list:
  171. # Case where `items` is a list of schemas.
  172. if idx < len(self.items):
  173. # Handle each item in the list.
  174. child_schema = self.items[idx]
  175. else:
  176. # Handle any additional items.
  177. if isinstance(self.additional_items, bool):
  178. break
  179. else:
  180. child_schema = self.additional_items
  181. error_items = child_schema.validate(item, context)
  182. errors += push_index(error_items, idx)
  183. if self.min_items is not None:
  184. if len(value) < self.min_items:
  185. if self.min_items == 1:
  186. errors += [self.make_error('empty')]
  187. else:
  188. errors += [self.make_error('min_items')]
  189. if self.max_items is not None:
  190. if len(value) > self.max_items:
  191. errors += [self.make_error('max_items')]
  192. if self.unique_items:
  193. if not(uniq(value)):
  194. errors += [self.make_error('unique')]
  195. return errors
  196. class Number(Schema):
  197. integer_only = False
  198. errors = {
  199. 'type': 'Must be a number.',
  200. 'minimum': 'Must be greater than or equal to {minimum}.',
  201. 'exclusive_minimum': 'Must be greater than {minimum}.',
  202. 'maximum': 'Must be less than or equal to {maximum}.',
  203. 'exclusive_maximum': 'Must be less than {maximum}.',
  204. 'multiple_of': 'Must be a multiple of {multiple_of}.',
  205. }
  206. def __init__(self, minimum=None, maximum=None, exclusive_minimum=False, exclusive_maximum=False, multiple_of=None, **kwargs):
  207. super(Number, self).__init__(**kwargs)
  208. self.minimum = minimum
  209. self.maximum = maximum
  210. self.exclusive_minimum = exclusive_minimum
  211. self.exclusive_maximum = exclusive_maximum
  212. self.multiple_of = multiple_of
  213. def validate(self, value, context=None):
  214. if isinstance(value, bool):
  215. # In Python `bool` subclasses `int`, so handle that case explicitly.
  216. return [self.make_error('type')]
  217. if not isinstance(value, numeric_types):
  218. return [self.make_error('type')]
  219. if self.integer_only and isinstance(value, float) and not value.is_integer():
  220. return [self.make_error('type')]
  221. errors = []
  222. if self.minimum is not None:
  223. if self.exclusive_minimum:
  224. if value <= self.minimum:
  225. errors += [self.make_error('exclusive_minimum')]
  226. else:
  227. if value < self.minimum:
  228. errors += [self.make_error('minimum')]
  229. if self.maximum is not None:
  230. if self.exclusive_maximum:
  231. if value >= self.maximum:
  232. errors += [self.make_error('exclusive_maximum')]
  233. else:
  234. if value > self.maximum:
  235. errors += [self.make_error('maximum')]
  236. if self.multiple_of is not None:
  237. if isinstance(self.multiple_of, float):
  238. failed = not (float(value) / self.multiple_of).is_integer()
  239. else:
  240. failed = value % self.multiple_of
  241. if failed:
  242. errors += [self.make_error('multiple_of')]
  243. return errors
  244. class Integer(Number):
  245. errors = {
  246. 'type': 'Must be an integer.',
  247. 'minimum': 'Must be greater than or equal to {minimum}.',
  248. 'exclusive_minimum': 'Must be greater than {minimum}.',
  249. 'maximum': 'Must be less than or equal to {maximum}.',
  250. 'exclusive_maximum': 'Must be less than {maximum}.',
  251. 'multiple_of': 'Must be a multiple of {multiple_of}.',
  252. }
  253. integer_only = True
  254. class String(Schema):
  255. errors = {
  256. 'type': 'Must be a string.',
  257. 'blank': 'Must not be blank.',
  258. 'max_length': 'Must have no more than {max_length} characters.',
  259. 'min_length': 'Must have at least {min_length} characters.',
  260. 'pattern': 'Must match the pattern /{pattern}/.',
  261. 'format': 'Must be a valid {format}.',
  262. }
  263. def __init__(self, max_length=None, min_length=None, pattern=None, format=None, **kwargs):
  264. super(String, self).__init__(**kwargs)
  265. self.max_length = max_length
  266. self.min_length = min_length
  267. self.pattern = pattern
  268. self.format = format
  269. self.pattern_regex = None
  270. if self.pattern is not None:
  271. self.pattern_regex = re.compile(pattern)
  272. def validate(self, value, context=None):
  273. if not isinstance(value, text_types):
  274. return [self.make_error('type')]
  275. errors = []
  276. if self.min_length is not None:
  277. if len(value) < self.min_length:
  278. if self.min_length == 1:
  279. errors += [self.make_error('blank')]
  280. else:
  281. errors += [self.make_error('min_length')]
  282. if self.max_length is not None:
  283. if len(value) > self.max_length:
  284. errors += [self.make_error('max_length')]
  285. if self.pattern is not None:
  286. if not re.search(self.pattern_regex, value):
  287. errors += [self.make_error('pattern')]
  288. if self.format is not None:
  289. if not validate_format(value, self.format):
  290. errors += [self.make_error('format')]
  291. return errors
  292. class Boolean(Schema):
  293. errors = {
  294. 'type': 'Must be a boolean.'
  295. }
  296. def validate(self, value, context=None):
  297. if not isinstance(value, bool):
  298. return [self.make_error('type')]
  299. return []
  300. class Null(Schema):
  301. errors = {
  302. 'type': 'Must be null.'
  303. }
  304. def validate(self, value, context=None):
  305. if value is not None:
  306. return [self.make_error('type')]
  307. return []
  308. class Enum(Schema):
  309. errors = {
  310. 'enum': 'Must be one of {enum}.',
  311. 'exact': 'Must be {exact}.',
  312. }
  313. def __init__(self, enum, **kwargs):
  314. super(Enum, self).__init__(**kwargs)
  315. self.enum = enum
  316. if len(enum) == 1:
  317. self.exact = repr(enum[0])
  318. def validate(self, value, context=None):
  319. if value not in self.enum:
  320. if len(self.enum) == 1:
  321. return [self.make_error('exact')]
  322. return [self.make_error('enum')]
  323. return []
  324. class Anything(Schema):
  325. errors = {
  326. 'type': 'Must be a valid primitive type.'
  327. }
  328. types = text_types + (dict, list, int, float, bool, type(None))
  329. def validate(self, value, context=None):
  330. if not isinstance(value, self.types):
  331. return [self.make_error('type')]
  332. errors = []
  333. if isinstance(value, list):
  334. schema = Array()
  335. errors += schema.validate(value, context)
  336. elif isinstance(value, dict):
  337. schema = Object()
  338. errors += schema.validate(value, context)
  339. return errors
  340. # Composites
  341. class Union(Schema):
  342. errors = {
  343. 'match': 'Must match one of the options.'
  344. }
  345. def __init__(self, children, **kwargs):
  346. super(Union, self).__init__(**kwargs)
  347. self.children = children
  348. def validate(self, value, context=None):
  349. for child in self.children:
  350. if child.validate(value, context) == []:
  351. return []
  352. return [self.make_error('match')]
  353. class Intersection(Schema):
  354. def __init__(self, children, **kwargs):
  355. super(Intersection, self).__init__(**kwargs)
  356. self.children = children
  357. def validate(self, value, context=None):
  358. errors = []
  359. for child in self.children:
  360. errors.extend(child.validate(value, context))
  361. return errors
  362. class ExclusiveUnion(Schema):
  363. errors = {
  364. 'match': 'Must match one of the options.',
  365. 'match_only_one': 'Must match only one of the options.'
  366. }
  367. def __init__(self, children, **kwargs):
  368. super(ExclusiveUnion, self).__init__(**kwargs)
  369. self.children = children
  370. def validate(self, value, context=None):
  371. matches = 0
  372. for child in self.children:
  373. if child.validate(value, context) == []:
  374. matches += 1
  375. if not matches:
  376. return [self.make_error('match')]
  377. elif matches > 1:
  378. return [self.make_error('match_only_one')]
  379. return []
  380. class Not(Schema):
  381. errors = {
  382. 'must_not_match': 'Must not match the option.'
  383. }
  384. def __init__(self, child, **kwargs):
  385. super(Not, self).__init__(**kwargs)
  386. self.child = child
  387. def validate(self, value, context=None):
  388. errors = []
  389. if self.child.validate(value, context):
  390. return []
  391. return [self.make_error('must_not_match')]
  392. # References
  393. class Ref(Schema):
  394. def __init__(self, ref_name):
  395. self.ref_name = ref_name
  396. def dereference(self, context):
  397. assert isinstance(context, dict)
  398. assert 'refs' in context
  399. assert self.ref_name in context['refs']
  400. return context['refs'][self.ref_name]
  401. def validate(self, value, context=None):
  402. schema = self.dereference(context)
  403. return schema.validate(value, context)
  404. class RefSpace(Schema):
  405. def __init__(self, refs, root):
  406. assert root in refs
  407. self.refs = refs
  408. self.root = root
  409. self.root_validator = refs[root]
  410. def validate(self, value):
  411. context = {'refs': self.refs}
  412. return self.root_validator.validate(value, context)