-
Notifications
You must be signed in to change notification settings - Fork 1
/
__init__.py
337 lines (277 loc) · 11.3 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# INITIALIZE THE CONTEXT
import pkg_resources
import PyV8
ajv_js = pkg_resources.resource_string( __name__, '/'.join(('js', "ajv.js")))
ctx = PyV8.JSContext()
ctx.enter()
ctx.eval(ajv_js)
def _eval(x):
return ctx.eval("(%s)"%x)
# ------------------------------
# HELPER FUNCTIONS BORROWED FROM:
# http://stackoverflow.com/a/28652754/1519199
# ------------------------------
def _build_accessor_string(ctx, route):
if len(route) == 0:
raise Exception("route must have at least one element")
accessor_string = ""
for elem in route:
if type(elem) in [str, unicode]:
accessor_string += "['" + elem + "']"
elif type(elem) == int:
accessor_string += "[" + str(elem) + "]"
else:
raise Exception("invalid element in route, must be text or number")
return accessor_string
def _get_py_obj(ctx, obj, route=[]):
# for handling of objects returned by Mongodb or MongoEngine
def dict_is_empty(dict):
for key in dict:
return False
return True
def access(obj, key):
if key in obj:
return obj[key]
return None
cloned = None
if isinstance(obj, list) or isinstance(obj, PyV8.JSArray):
cloned = []
if len(route):
_ = str(_build_accessor_string(ctx, route)) #working around a problem with PyV8 r429
for i in range(len(obj)):
elem = obj[i]
cloned.append(_get_py_obj(ctx, elem, route + [i]))
elif isinstance(obj, dict) or isinstance(obj, PyV8.JSObject):
cloned = {}
for key in obj.keys():
cloned_val = None
if type(key) == int:
#workaround for a problem with PyV8 where it won't let me access
#objects with integer accessors
val = None
try:
val = access(obj, str(key))
except KeyError:
pass
if val == None:
val = access(obj, key)
cloned_val = _get_py_obj(ctx, val, route + [key])
else:
cloned_val = _get_py_obj(ctx, access(obj, key), route + [key])
cloned[key] = cloned_val
elif isinstance(obj,basestring):
cloned = obj.decode('utf-8')
else:
cloned = obj
return cloned
def __validate_npm_package_name(x):
return False
def load(filepath):
if __validate_npm_package_name(filepath):
filepath = _os.path.join(__dir,"plugin",filepath + ".js")
assert _os.path.exists(filepath), ("No such file: " + filepath)
with open(filepath) as fh:
return ctx.eval("(function(){module = {exports:{}};exports = module.exports;%s;return module.exports;})()"
% fh.read())
import os as _os
__dir = _os.path.dirname(_os.path.realpath(__file__))
__validator_path = _os.path.join(__dir, 'js/validate-npm-package-name.js')
__is_valid_npm_package_name = load(__validator_path)
def __validate_npm_package_name(x):
assert isinstance(x, basestring)
return _get_py_obj(ctx,__is_valid_npm_package_name(x))["validForOldPackages"]
def _get_js_obj(ctx,obj):
#workaround for a problem with PyV8 where it will implicitely convert python lists to js objects
#-> we need to explicitely do the conversion. see also the wrapper classes for JSContext above.
if isinstance(obj, list):
js_list = []
for entry in obj:
js_list.append(_get_js_obj(ctx,entry))
return PyV8.JSArray(js_list)
elif isinstance(obj, dict):
js_obj = ctx.eval("new Object();")
for key in obj.keys():
try:
js_obj[key] = _get_js_obj(ctx,obj[key])
except Exception, e:
# unicode keys raise a Boost.python.aubment Exception which
# can't be caught directly:
# https://mail.python.org/pipermail/cplusplus-sig/2010-April/015470.html
if (not str(e).startswith("Python argument types in")):
raise
import unicodedata
js_obj[unicodedata.normalize('NFKD', key).encode('ascii','ignore')] = _get_js_obj(ctx,obj[key])
return js_obj
else:
return obj
# THE MAIN EXPORT
class Ajv(object):
def __init__(self,options=None):
# create the AJV instance in V8
if options is None:
self.inst = ctx.eval("new Ajv()")
else:
ctx.locals["_options"] = _get_js_obj(ctx,options)
self.inst = ctx.eval("new Ajv(_options)")
del ctx.locals["_options"]
# --------------------------------------------------
# NON-NATIVE METHODS AND ATTRIBUTES
# --------------------------------------------------
# --------------------
# Plugin
# --------------------
def plugin(self,x,options=None):
""" the Ajv.validate method
The equivalent of calling \code{var ajv = new Ajv(); ajv.validate(...)} in javascript.
@param schema The Schema with which to validate the \code{data}.
@param data The data to be validated. may be any of the above foremats.
@return boolean
"""
if isinstance(x, basestring):
x = load(x)
assert x.__class__.__name__ == 'JSFunction', "x must be a string (file path) or loaded JS module"
if options is not None:
x(self.inst,_get_js_obj(ctx,options))
else:
x(self.inst)
# --------------------
# LAST
# --------------------
@property
def last(self):
""" returns the last object validated, for compatibility with modifying keywords
"""
try:
return _get_py_obj(ctx,self.__last)
except AttributeError:
raise AttributeError("'Ajv' has no attribute `last`; the property is only available after validating an object")
# --------------------------------------------------
# METHODS THAT RETURN A BOOLEAN
# --------------------------------------------------
def validate(self,schema,data):
""" the Ajv.validate method
The equivalent of calling \code{var ajv = new Ajv(); ajv.validate(...)} in javascript.
@param schema The Schema with which to validate the \code{data}.
@param data The data to be validated. may be any of the above foremats.
@return boolean
"""
self.__last = _get_js_obj(ctx,data)
return self.inst.validate(_get_js_obj(ctx,schema),self.__last)
def validateSchema(self,schema):
""" the Ajv.validateSchema method
The validate a json schema
@param schema The Schema to be validated.
@return boolean
"""
return self.inst.validateSchema(_get_js_obj(ctx,schema))
# --------------------------------------------------
# METHODS THAT RETURN an Object
# --------------------------------------------------
def compile(self,schema):
""" The Ajv.compile method
@return a callable ajv validator object
"""
return validator(self.inst.compile(_get_js_obj(ctx,schema)))
def getSchema(self,key):
""" The Ajv.getSchema method
@return a callable ajv validator object
"""
return validator(self.inst.getSchema(_get_js_obj(ctx,key)))
def errorsText(self):
""" The Ajv.errorsText method
Extracts the errors obj from the instance
@return object containing the error message (if any)
"""
return _get_py_obj(ctx,self.inst.errorsText())
# --------------------------------------------------
# METHODS THAT RETURN None
# --------------------------------------------------
def addSchema(self,key,schema):
""" the Ajv.addSchema method
The add a schema to an Ajv instance
@param schema The schema to be added.
@param key String; the name with which to store the schema
@return None
"""
self.inst.addSchema(_get_js_obj(ctx,key),_get_js_obj(ctx,schema))
def removeSchema(self,key):
""" the Ajv.removeSchema method
The remove a schema from an Ajv instance
@param key String; the name with schema to remove
@return None
"""
self.inst.removeSchema(_get_js_obj(ctx,key))
def addFormat(self,key,format):
""" the Ajv.addFormat method
Add a string format to an Ajv instance.
@param key String; the name with format to add.
@param format the format to be added.
@return None
"""
self.inst.addFormat(_get_js_obj(ctx,key))
def keyword(self,key,obj):
""" the Ajv.addFormat method
Add a string format to an Ajv instance.
@param key String; the name with keyword to add.
@param obj the format to be added.
@return None
"""
self.inst.keyword(_get_js_obj(ctx,key),_get_js_obj(ctx,obj))
def addKeyword(self,name,definition):
""" the Ajv.addKeyword method
The add a schema to an Ajv instance
@param name The name of the keyword to be added.
@param definition A string encoding of a javascript obj to be used as to define the keyword.
@return None
"""
self.inst.addKeyword(_get_js_obj(ctx,name),_get_js_obj(ctx,definition))
def __getattribute__(self, name):
if name == "errors":
return _get_py_obj(ctx,self.inst.errors)
else:
return object.__getattribute__(self, name)
class validator(object):
def __init__(self,inst):
self.inst = inst
def __call__(self,data):
self.__last = _get_js_obj(ctx,data)
return self.inst(self.__last);
def __getattribute__(self, name):
if name == "errors":
return _get_py_obj(ctx,self.inst.errors)
else:
return object.__getattribute__(self, name)
# --------------------------------------------------
# NON-NATIVE METHODS AND ATTRIBUTES
# --------------------------------------------------
@property
def last(self):
""" returns the last object validated, for compatibility with modifying keywords
"""
try:
return _get_py_obj(ctx,self.__last)
except AttributeError:
raise AttributeError("'Validator' has no attribute `last`; the property is only available after validating an object")
# a decorator for the clean method of MongoEngine.Document class
_ajv = None
def ajv_clean(schema,ajv=None):
global _ajv
from mongoengine import ValidationError
if ajv is None:
if _ajv is None:
_ajv = Ajv()
ajv = _ajv
validator = ajv.compile(schema)
def decorator(f):
def wrapper(self):
# execute ajv validator
if not validator(dict(self.to_mongo())):
msg = ", and ".join(["'%s' %s" %(e["schemaPath"],e["message"]) for e in validator.errors ])
msg = ", and ".join(["'%s' %s" %(e["schemaPath"],e["message"]) for e in validator.errors ])
err = ValidationError("AJV validation failed with message(s): "+msg)
err.ajv_errors = validator.errors
raise err
# execute the normal Document.clean code
f(self,validator.last)
return wrapper
return decorator