Obviously you could use an ajvpy validator to validate documents directly, like this:
# imports
from mongoengine import *
from ajvpy import Ajv
# Instantiate an AJV validator
ajv = Ajv()
my_ajv_schema = {
"type":"object",
"required":["a","b"]
}
my_validator = ajv.compile(my_ajv_schema)
# Create a Document Class with a Clean Method that uses your validator
class My_document(Document):
status = StringField(choices=('Published', 'Draft'), required=True)
...
def clean(self):
if not my_validator(self):
raise ValidationError("Failed to validate against AJV schema.")
or you could use the ajv_clean
decorator included in the ajvpy
module:
from mongoengine import *
from ajvpy import ajv_clean
class My_document(Document):
status = StringField(choices=('Published', 'Draft'), required=True)
...
# Have the decorator instantiate the validator and validate each document instance
@ajv_clean(my_ajv_schema)
def clean(self,validated):
pass
Note that because (1) the data are validated in the PyV8
runtime, and (2) the object can be modified using certian Ajv keywords, the wrapped Document.clean
method should take two arguments: The mongoengne.Document
instance being validated (self
) and the validated object (validated
) that made the round trip from Python to JavaScript and back, which can be used to pass an updates or additions made by Ajv to the mongoengine.Document
instnace (see the example below).
If you want to extend the ajv instance used for validation with plugin modules, use:
from mongoengine import *
from ajvpy import Ajv, ajv_clean
# Create and extend an Ajv instance
ajv = Ajv()
ajv.plugin("my-plugin")
class My_document(Document):
status = StringField(choices=('Published', 'Draft'), required=True)
...
# Pass the extended Ajv instance to the decorator
@ajv_clean(my_ajv_schema,ajv)
def clean(self,validated):
pass
AJV schemas are just objects, so we can store them in a mongodb collection
using MongoEngine
. We can combine this with the ajv_clean
wrapper to
use stored schema when creating new mongoengine.Document
classes
We'll use semver strings to keep track of schema versions, so to get
started we'll need to clone ajvpy and then download and compile the
ajv-semver
plugin from NPM so it's available to use in ajvpy
.
git clone https://github.com/jdthorpe/ajvpy
cd ajvpy
npm install
npm install ajv-semver
npm run bundle -- ajv-semver
cd ../
With the required Ajv plugins compiled, we can use it to validate Documents
with MongoEngine when they are doc.clean()
'd or doc.save()
'd
# IMPORTS
from mongoengine import *
from ajvpy import Ajv, ajv_clean
# CREATE AN EXTENDED INSTANCE WITH THE "semver" KEYWORD
ajv = Ajv({"useDefaults":True})
ajv.plugin("ajv-semver")
# A SCHEMA FOR OUR THE COLLECTION THAT WILL STORE OUR FAVORITE SCHEMA
ajv_schema_schema = {
"type":"object",
"properties":{
"name":{
"type":"string",
# schema names should consist of lower case letters
# broken by single dashes or underscores, but not both
"pattern":"^[a-z]+(?:(?:-[a-z]+)+|(?:_[a-z]+)+)?$",
},
"version":{
# version should be a Semver string
"semver":{"clean":True},
},
# schema should be a valid JSON Schema
"schema":{
"$ref": "http://json-schema.org/draft-06/schema#",
},
# Automatically calculate the Major, Minor, and Patch of the version
"major":{
"default": None, # required to create a new attribute
"semver":{"major":{"$data":"1/version"},"loose":True}
},
"minor":{
"default": None, # required to create a new attribute
"semver":{"minor":{"$data":"1/version"},"loose":True}
},
"patch":{
"default": None, # required to create a new attribute
"semver":{"patch":{"$data":"1/version"},"loose":True}
},
# SCHEMA MAY REQUIRE AJV options
"options":{ "type": "object" },
# SCHEMA MAY REQUIRE A PLUGIN OR TWO.
# specifically, plugins may be an object like one of these:
#
# - {"my-plugin":true} #>> "my-plugin" plugin is required
#
# - {"my-plugin":"^3.2.1"} #>> version "^3.2.1" of "my-plugin" plugin is required
#
# - {
# "my-plugin":{
# "version":"^3.2.1", #>> version "^3.2.1" of "my-plugin" plugin is required
# "options":{"foo":"bar"} #>> "my-plugin" plugin should be loaded with the options {"foo":"bar"}
# }
# }
"plugins":{
"type": "object",
"additionalProperties":{
"oneOf":[
# no particular version or options required
{"type":"boolean"},
# required plugin version
{"type":"string","semver":True},
# required version and/or options object
{
"type":"object",
"properties":{
"version":{"type":"string","semver":True},
"options":{"type":"object"} # any
},
"anyOf":[
{"required":["version"]},
{"required":["options"]}
]
},
]
}
}
},
"required":[
"name",
"version",
"schema",
],
}
# CREATE DOCUMENT CLASS FOR THE STORING OUR FAVORITE AJV SCHEMAS
class ajvSchema(Document):
# store the schema in the `--ajv-schema` collection
meta = {'collection': '--ajv-schema'}
# Fields
name = StringField(required=True)
version = StringField(required=True)
schema = DictField(required=True)
options = DictField(required=False,default=None)
plugins = DictField(required=False,default=None)
major = IntField(required=False)
minor = IntField(required=False)
patch = IntField(required=False)
# pass the Ajv instance extended with the "semver" keyword
@ajv_clean(ajv_schema_schema, ajv)
def clean(self,validated):
# Gather the major, minor and patch calculated by ajv and the
# cleaned up version string from the `validated` object which
# made the round trip from python to JavaScript and back.
self.version = validated["version"]
self.major = validated["major"]
self.minor = validated["minor"]
self.patch = validated["patch"]
and now you can store your favorite AJV schema along side your data, like so:
# an Ajv-schema we'd like to store
contact_schema = {
"type":"object",
"properties":{
"name": { "type":"string" },
"email": { "type":"string" ,"format":"email"},
"birthday": { "type":"string" ,"format":"date"},
"website": { "type":"string" ,"format":"url"},
},
"required":["name","email"]
}
# create a new MongoEngine ajvSchema Document with the new schema
good = ajvSchema(name="my-contacts",version="=1.0.0",schema=contact_schema)
# Does this document match the schema?
good.clean() # Yep, no exception is raised!
# note the version string was cleaned up and that the major, minor and
# patch have been added to the object -- handy for indexing by version.
good.version # "1.0.0"
good.major # 1
good.minor # 0
good.patch # 0
# now connect to the database 'My-project' and save
connect("my-project")
good.save() #>> Stores the object
# count the stored objects
ajvSchema.objects.count() # 1
# create a few more objects
ajvSchema(name="my-contacts",version="1.2.3",schema=contact_schema).save()
ajvSchema(name="my-contacts",version="1.2.4",schema=contact_schema).save()
ajvSchema(name="my-contacts",version="1.6.3",schema=contact_schema).save()
ajvSchema(name="my-contacts",version="2.6.3",schema=contact_schema).save()
# Count by version
ajvSchema.objects(major=1).count()
ajvSchema.objects(major=2).count()
ajvSchema.objects(minor=6).count()
# get the latest version
ajvSchema.objects.order_by('-major', '-minor','-patch')[0].version
# get the latest of the version 1.x.x series
ajvSchema.objects(major=1).order_by( '-minor','-patch')[0].version
In another Python session use the latest version to populate a validator!
from mongoengine import *
from ajvpy import Ajv, ajv_clean
# Even better: create a module with the definition of ajvSchema and call:
# from my_schema import ajvSchema
class ajvSchema(Document):
# store the schema in the `--ajv-schema` collection
meta = {'collection': '--ajv-schema'}
name = StringField(required=True)
version = StringField(required=True)
schema = DictField(required=True)
options = DictField(required=False,default=None)
plugins = DictField(required=False,default=None)
major = IntField(required=False)
minor = IntField(required=False)
patch = IntField(required=False)
# Connect to the database
connect("my-project")
# Fetch the latest version of our Ajv schema and options
latest_contact_schema = ajvSchema.objects.order_by('-major', '-minor','-patch')[0]
# Instantiate an ajv instance with any required options
ajv_inst = Ajv(latest_contact_schema.options)
# Add in any required plugins
plugin_options = latest_contact_schema.plugins
if plugin_options is not None:
for name in plugin_options.keys():
if isinstance(plugin_options[name],(bool,basestring)):
ajv_inst.plugin(name)
else:
ajv_inst.plugin(
name,
plugin_options[name].get("options",None))
# create the contacts class to be validated with the stored validator
class Contact(Document):
name = StringField(required=True)
email = StringField(required=True)
birthday = StringField(required=False,defautl=None)
website = StringField(required=False)
# pass the Ajv instance extended with the "semver" keyword
@ajv_clean(latest_contact_schema.schema, ajv_inst)
def clean(self,validated):
pass
my_contact = Contact(
name="Oscar Madison",
email="[email protected]",
website="https://www.google.com/search?q=oscar+madison")
my_contact.clean()
my_contact.save()
Contact(name="Oscar Madison",
email="[email protected]",
website="ttps://www.google.com/search?q=oscar+madison" # bad url
).clean() # raises: '#/properties/website/format' should match format "url"
Contact(name="Oscar Madison",
email="oscar_at_poker.com", # bad email
).clean() # raises: '#/properties/email/format' should match format "email"