Skip to content

Commit

Permalink
Merge pull request #104 from kda47/master
Browse files Browse the repository at this point in the history
Fixed marshmallow type subclass check for datetime based classes, added few simple additional options
  • Loading branch information
fuhrysteve authored Jan 16, 2020
2 parents 81cc98d + a500e22 commit 407ae7f
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ ENV/
.spyderproject
.spyproject

# Vscode project settings
.vscode

# Rope project settings
.ropeproject

Expand Down
30 changes: 25 additions & 5 deletions marshmallow_jsonschema/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
RAISE,
)
from .exceptions import UnsupportedValueError
from .validation import handle_length, handle_one_of, handle_range
from .validation import handle_length, handle_one_of, handle_range, handle_regexp

__all__ = ("JSONSchema",)

Expand Down Expand Up @@ -52,7 +52,6 @@
# This part of a mapping is carefully selected from marshmallow source code,
# see marshmallow.BaseSchema.TYPE_MAPPING.
(fields.String, text_type),
(fields.DateTime, datetime.datetime),
(fields.Float, float),
(fields.Raw, text_type),
(fields.Boolean, bool),
Expand All @@ -61,6 +60,7 @@
(fields.Time, datetime.time),
(fields.Date, datetime.date),
(fields.TimeDelta, datetime.timedelta),
(fields.DateTime, datetime.datetime),
(fields.Decimal, decimal.Decimal),
# These are some mappings that generally make sense for the rest
# of marshmallow fields.
Expand All @@ -78,6 +78,7 @@
validate.Length: handle_length,
validate.OneOf: handle_one_of,
validate.Range: handle_range,
validate.Regexp: handle_regexp,
}


Expand Down Expand Up @@ -112,16 +113,29 @@ class JSONSchema(Schema):
required = fields.Method("get_required")

def __init__(self, *args, **kwargs):
"""Setup internal cache of nested fields, to prevent recursion."""
"""Setup internal cache of nested fields, to prevent recursion.
:param bool props_ordered: if `True` order of properties will be save as declare in class,
else will using sorting, default is `False`.
Note: For the marshmallow scheme, also need to enable
ordering of fields too (via `class Meta`, attribute `ordered`).
"""
self._nested_schema_classes = {}
self.nested = kwargs.pop("nested", False)
self.props_ordered = kwargs.pop("props_ordered", False)
setattr(self.opts, "ordered", self.props_ordered)
super(JSONSchema, self).__init__(*args, **kwargs)

def get_properties(self, obj):
"""Fill out properties field."""
properties = {}
properties = self.dict_class()

for field_name, field in sorted(obj.fields.items()):
if self.props_ordered:
fields_items_sequence = obj.fields.items()
else:
fields_items_sequence = sorted(obj.fields.items())

for field_name, field in fields_items_sequence:
schema = self._get_schema_for_field(obj, field)
properties[field.metadata.get("name") or field.name] = schema

Expand Down Expand Up @@ -190,6 +204,12 @@ def _get_schema_for_field(self, obj, field):
schema = FIELD_VALIDATORS[validator.__class__](
schema, field, validator, obj
)
else:
base_class = getattr(
validator, "_jsonschema_base_validator_class", None
)
if base_class is not None and base_class in FIELD_VALIDATORS:
schema = FIELD_VALIDATORS[base_class](schema, field, validator, obj)
return schema

def _from_nested_schema(self, obj, field):
Expand Down
32 changes: 32 additions & 0 deletions marshmallow_jsonschema/validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,3 +120,35 @@ def handle_range(schema, field, validator, parent_schema):
else:
schema["exclusiveMaximum"] = validator.max
return schema


def handle_regexp(schema, field, validator, parent_schema):
"""Adds validation logic for ``marshmallow.validate.Regexp``, setting the
values appropriately ``fields.String`` and it's subclasses.
Args:
schema (dict): The original JSON schema we generated. This is what we
want to post-process.
field (fields.Field): The field that generated the original schema and
who this post-processor belongs to.
validator (marshmallow.validate.Regexp): The validator attached to the
passed in field.
parent_schema (marshmallow.Schema): The Schema instance that the field
belongs to.
Returns:
dict: New JSON Schema that has been post processed and
altered.
Raises:
UnsupportedValueError: Raised if the `field` is not an instance of
`fields.String`.
"""
if not isinstance(field, fields.String):
raise UnsupportedValueError(
"'Regexp' validator for non-string fields is not supported"
)

schema["pattern"] = validator.regex.pattern

return schema
63 changes: 63 additions & 0 deletions tests/test_dump.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from marshmallow import Schema, fields, validate

from marshmallow_jsonschema import JSONSchema, UnsupportedValueError
from marshmallow_jsonschema.compat import dot_data_backwards_compatible
from . import UserSchema, validate_and_dump


Expand Down Expand Up @@ -429,3 +430,65 @@ class TestSchema(Schema):
dumped = validate_and_dump(schema)

assert "required" not in dumped["definitions"]["TestSchema"]


def test_datetime_based():
class TestSchema(Schema):
f_date = fields.Date()
f_datetime = fields.DateTime()
f_time = fields.Time()

schema = TestSchema()

dumped = validate_and_dump(schema)

assert dumped["definitions"]["TestSchema"]["properties"]["f_date"] == {
"format": "date",
"title": "f_date",
"type": "string",
}

assert dumped["definitions"]["TestSchema"]["properties"]["f_datetime"] == {
"format": "date-time",
"title": "f_datetime",
"type": "string",
}

assert dumped["definitions"]["TestSchema"]["properties"]["f_time"] == {
"format": "time",
"title": "f_time",
"type": "string",
}


def test_sorting_properties():
class TestSchema(Schema):
class Meta:
ordered = True

d = fields.Str()
c = fields.Str()
a = fields.Str()

# Should be sorting of fields
schema = TestSchema()

json_schema = JSONSchema()
dumped = json_schema.dump(schema)
data = dot_data_backwards_compatible(dumped)

sorted_keys = sorted(data["definitions"]["TestSchema"]["properties"].keys())
properties_names = [k for k in sorted_keys]
assert properties_names == ["a", "c", "d"]

# Should be saving ordering of fields
schema = TestSchema()

json_schema = JSONSchema(props_ordered=True)
dumped = json_schema.dump(schema)
data = dot_data_backwards_compatible(dumped)

keys = data["definitions"]["TestSchema"]["properties"].keys()
properties_names = [k for k in keys]

assert properties_names == ["d", "c", "a"]
46 changes: 46 additions & 0 deletions tests/test_validation.py
Original file line number Diff line number Diff line change
Expand Up @@ -126,3 +126,49 @@ class TestSchema(Schema):

with pytest.raises(UnsupportedValueError):
json_schema.dump(schema)


def test_regexp():
ipv4_regex = (
r"^(([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])\.){3}"
r"([0-9]|[1-9][0-9]|1[0-9]{2}|2[0-4][0-9]|25[0-5])$"
)

class TestSchema(Schema):
ip_address = fields.String(validate=validate.Regexp(ipv4_regex))

schema = TestSchema()

dumped = validate_and_dump(schema)

assert dumped["definitions"]["TestSchema"]["properties"]["ip_address"] == {
"title": "ip_address",
"type": "string",
"pattern": ipv4_regex,
}


def test_regexp_error():
class TestSchema(Schema):
test_regexp = fields.Int(validate=validate.Regexp(r"\d+"))

schema = TestSchema()

with pytest.raises(UnsupportedValueError):
dumped = validate_and_dump(schema)


def test_custom_validator():
class TestValidator(validate.Range):
_jsonschema_base_validator_class = validate.Range

class TestSchema(Schema):
test_field = fields.Int(validate=TestValidator(min=1, max=10))

schema = TestSchema()

dumped = validate_and_dump(schema)

props = dumped["definitions"]["TestSchema"]["properties"]
assert props["test_field"]["minimum"] == 1
assert props["test_field"]["maximum"] == 10

0 comments on commit 407ae7f

Please sign in to comment.