Skip to content

Adding support for readOnly and writeOnly #135

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 8 commits into from
Closed
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ nosetests.xml
coverage.xml
*.cover
.hypothesis/
reports/

# Translations
*.mo
Expand Down
4 changes: 2 additions & 2 deletions openapi_core/schema/media_types/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def deserialize(self, value):
deserializer = self.get_dererializer()
return deserializer(value)

def unmarshal(self, value, custom_formatters=None):
def unmarshal(self, value, custom_formatters=None, read=False, write=False):
if not self.schema:
return value

Expand All @@ -47,6 +47,6 @@ def unmarshal(self, value, custom_formatters=None):
raise InvalidMediaTypeValue(exc)

try:
return self.schema.validate(unmarshalled, custom_formatters=custom_formatters)
return self.schema.validate(unmarshalled, custom_formatters=custom_formatters, read=read, write=write)
except OpenAPISchemaError as exc:
raise InvalidMediaTypeValue(exc)
2 changes: 1 addition & 1 deletion openapi_core/schema/parameters/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,7 @@ def get_value(self, request):

return location[self.name]

def unmarshal(self, value, custom_formatters=None):
def unmarshal(self, value, custom_formatters=None, **kwargs):
if self.deprecated:
warnings.warn(
"{0} parameter is deprecated".format(self.name),
Expand Down
3 changes: 3 additions & 0 deletions openapi_core/schema/schemas/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,8 @@ def create(self, schema_spec):
exclusive_maximum = schema_deref.get('exclusiveMaximum', False)
min_properties = schema_deref.get('minProperties', None)
max_properties = schema_deref.get('maxProperties', None)
read_only = schema_deref.get('readOnly', False)
write_only = schema_deref.get('writeOnly', False)

properties = None
if properties_spec:
Expand Down Expand Up @@ -76,6 +78,7 @@ def create(self, schema_spec):
exclusive_maximum=exclusive_maximum,
exclusive_minimum=exclusive_minimum,
min_properties=min_properties, max_properties=max_properties,
read_only=read_only, write_only=write_only
)

@property
Expand Down
52 changes: 33 additions & 19 deletions openapi_core/schema/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import re
import warnings

from six import iteritems, integer_types, binary_type, text_type
from six import iteritems, integer_types, binary_type, text_type, string_types

from openapi_core.extensions.models.factories import ModelFactory
from openapi_core.schema.schemas.enums import SchemaFormat, SchemaType
Expand Down Expand Up @@ -42,14 +42,14 @@ class Schema(object):
}

STRING_FORMAT_CALLABLE_GETTER = {
SchemaFormat.NONE: Format(text_type, TypeValidator(text_type)),
SchemaFormat.PASSWORD: Format(text_type, TypeValidator(text_type)),
SchemaFormat.NONE: Format(text_type, TypeValidator(string_types)),
SchemaFormat.PASSWORD: Format(text_type, TypeValidator(string_types)),
SchemaFormat.DATE: Format(
format_date, TypeValidator(date, exclude=datetime)),
SchemaFormat.DATETIME: Format(format_datetime, TypeValidator(datetime)),
SchemaFormat.BINARY: Format(binary_type, TypeValidator(binary_type)),
SchemaFormat.UUID: Format(format_uuid, TypeValidator(UUID)),
SchemaFormat.BYTE: Format(format_byte, TypeValidator(text_type)),
SchemaFormat.BYTE: Format(format_byte, TypeValidator(string_types)),
}

NUMBER_FORMAT_CALLABLE_GETTER = {
Expand Down Expand Up @@ -79,7 +79,8 @@ def __init__(
min_length=None, max_length=None, pattern=None, unique_items=False,
minimum=None, maximum=None, multiple_of=None,
exclusive_minimum=False, exclusive_maximum=False,
min_properties=None, max_properties=None):
min_properties=None, max_properties=None, read_only=False,
write_only=False):
self.type = SchemaType(schema_type)
self.model = model
self.properties = properties and dict(properties) or {}
Expand Down Expand Up @@ -109,6 +110,8 @@ def __init__(
if min_properties is not None else None
self.max_properties = int(max_properties)\
if max_properties is not None else None
self.read_only = read_only
self.write_only = write_only
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This allows a given schema to be both readOnly and writeOnly at the same time.


self._all_required_properties_cache = None
self._all_optional_properties_cache = None
Expand Down Expand Up @@ -391,7 +394,7 @@ def default(x, **kw):

return defaultdict(lambda: default, mapping)

def validate(self, value, custom_formatters=None):
def validate(self, value, custom_formatters=None, read=False, write=False):
if value is None:
if not self.nullable:
raise InvalidSchemaValue("Null value for non-nullable schema of type {type}", value, self.type)
Expand All @@ -407,11 +410,11 @@ def validate(self, value, custom_formatters=None):
# structure validation
validator_mapping = self.get_validator_mapping()
validator_callable = validator_mapping[self.type]
validator_callable(value, custom_formatters=custom_formatters)
validator_callable(value, custom_formatters=custom_formatters, read=read, write=write)

return value

def _validate_collection(self, value, custom_formatters=None):
def _validate_collection(self, value, custom_formatters=None, **kwargs):
if self.items is None:
raise UndefinedItemsSchema(self.type)

Expand Down Expand Up @@ -439,10 +442,10 @@ def _validate_collection(self, value, custom_formatters=None):
raise OpenAPISchemaError("Value may not contain duplicate items")

f = functools.partial(self.items.validate,
custom_formatters=custom_formatters)
custom_formatters=custom_formatters, **kwargs)
return list(map(f, value))

def _validate_number(self, value, custom_formatters=None):
def _validate_number(self, value, **kwargs):
if self.minimum is not None:
if self.exclusive_minimum and value <= self.minimum:
raise InvalidSchemaValue(
Expand All @@ -464,7 +467,7 @@ def _validate_number(self, value, custom_formatters=None):
"Value {value} is not a multiple of {type}",
value, self.multiple_of)

def _validate_string(self, value, custom_formatters=None):
def _validate_string(self, value, custom_formatters=None, **kwargs):
try:
schema_format = SchemaFormat(self.format)
except ValueError:
Expand Down Expand Up @@ -513,7 +516,7 @@ def _validate_string(self, value, custom_formatters=None):

return True

def _validate_object(self, value, custom_formatters=None):
def _validate_object(self, value, custom_formatters=None, **kwargs):
properties = value.__dict__

if self.one_of:
Expand All @@ -522,7 +525,7 @@ def _validate_object(self, value, custom_formatters=None):
try:
self._validate_properties(
properties, one_of_schema,
custom_formatters=custom_formatters)
custom_formatters=custom_formatters, **kwargs)
except OpenAPISchemaError:
pass
else:
Expand All @@ -535,7 +538,7 @@ def _validate_object(self, value, custom_formatters=None):

else:
self._validate_properties(properties,
custom_formatters=custom_formatters)
custom_formatters=custom_formatters, **kwargs)

if self.min_properties is not None:
if self.min_properties < 0:
Expand Down Expand Up @@ -565,7 +568,7 @@ def _validate_object(self, value, custom_formatters=None):
return True

def _validate_properties(self, value, one_of_schema=None,
custom_formatters=None):
custom_formatters=None, read=False, write=False):
all_props = self.get_all_properties()
all_props_names = self.get_all_properties_names()
all_req_props_names = self.get_all_required_properties_names()
Expand All @@ -588,19 +591,30 @@ def _validate_properties(self, value, one_of_schema=None,
for prop_name in extra_props:
prop_value = value[prop_name]
self.additional_properties.validate(
prop_value, custom_formatters=custom_formatters)
prop_value, custom_formatters=custom_formatters,
read=read, write=write)

for prop_name, prop in iteritems(all_props):
should_skip = (write and prop.read_only) or (read and prop.write_only)
try:
prop_value = value[prop_name]
if read and prop.write_only:
message = "WriteOnly property {prop} defined on read.".format(prop=prop_name)
raise UndefinedSchemaProperty(message)

if write and prop.read_only:
message = "ReadOnly property {prop} defined on write.".format(prop=prop_name)
raise UndefinedSchemaProperty(message)

except KeyError:
if prop_name in all_req_props_names:
if prop_name in all_req_props_names and not should_skip:
raise MissingSchemaProperty(prop_name)
if not prop.nullable and not prop.default:
if (not prop.nullable and not prop.default) or should_skip:
continue
prop_value = prop.default
try:
prop.validate(prop_value, custom_formatters=custom_formatters)
prop.validate(prop_value, custom_formatters=custom_formatters,
read=read, write=write)
except OpenAPISchemaError as exc:
raise InvalidSchemaProperty(prop_name, original_exception=exc)

Expand Down
4 changes: 2 additions & 2 deletions openapi_core/validation/request/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ def _get_parameters(self, request, operation):
continue

try:
value = param.unmarshal(raw_value, self.custom_formatters)
value = param.unmarshal(raw_value, self.custom_formatters, read=True)
except OpenAPIMappingError as exc:
errors.append(exc)
else:
Expand All @@ -79,7 +79,7 @@ def _get_body(self, request, operation):
errors.append(exc)
else:
try:
body = media_type.unmarshal(raw_body, self.custom_formatters)
body = media_type.unmarshal(raw_body, self.custom_formatters, write=True)
except OpenAPIMappingError as exc:
errors.append(exc)

Expand Down
2 changes: 1 addition & 1 deletion openapi_core/validation/response/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ def _get_data(self, response, operation_response):
errors.append(exc)
else:
try:
data = media_type.unmarshal(raw_data, self.custom_formatters)
data = media_type.unmarshal(raw_data, self.custom_formatters, read=True)
except OpenAPIMappingError as exc:
errors.append(exc)

Expand Down
52 changes: 52 additions & 0 deletions tests/integration/data/v3.0/get_and_post.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test Post and Get
license:
name: MIT
paths:
/object:
post:
summary: Post an Object
operationId: postObject
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ObjectDesc'
responses:
'201':
description: Null response
/object/{objectId}:
get:
summary: Get an Object
operationId: getObject
parameters:
- name: objectId
in: path
required: true
description: The id of the object
schema:
type: string
responses:
'200':
description: Object description
content:
application/json:
schema:
$ref: "#/components/schemas/ObjectDesc"
components:
schemas:
ObjectDesc:
type: object
additionalProperties: False
properties:
object_id:
type: string
readOnly: true
message:
type: string
password:
type: string
writeOnly: true
75 changes: 75 additions & 0 deletions tests/integration/test_get_and_post.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import pytest
import json

from openapi_core.shortcuts import create_spec
from openapi_core.validation.request.validators import RequestValidator
from openapi_core.validation.response.validators import ResponseValidator
from openapi_core.wrappers.mock import MockRequest, MockResponse


class TestGetAndPost(object):

get_object = [{
"object_id": "random_id",
"message": "test message"
}]

post_object = [{
"message": "second message",
"password": "fakepassword"
}]
spec_paths = [
"data/v3.0/get_and_post.yaml",
]

@pytest.mark.parametrize("response", post_object)
@pytest.mark.parametrize("spec_path", spec_paths)
def test_post_object_success(self, factory, response, spec_path):
spec_dict = factory.spec_from_file(spec_path)
spec = create_spec(spec_dict)
validator = RequestValidator(spec)
request = MockRequest("http://www.example.com", "post",
"/object", data=json.dumps(response))

result = validator.validate(request)
assert not result.errors

@pytest.mark.parametrize("response", get_object)
@pytest.mark.parametrize("spec_path", spec_paths)
def test_post_object_failure(self, factory, response, spec_path):
spec_dict = factory.spec_from_file(spec_path)
spec = create_spec(spec_dict)
validator = RequestValidator(spec)
request = MockRequest("http://www.example.com", "post",
"/object", data=json.dumps(response))

result = validator.validate(request)
assert result.errors

@pytest.mark.parametrize("response", get_object)
@pytest.mark.parametrize("spec_path", spec_paths)
def test_get_object_success(self, factory, response, spec_path):
spec_dict = factory.spec_from_file(spec_path)
spec = create_spec(spec_dict)
request = MockRequest("http://www.example.com", "get",
"/object/{objectId}")
validator = ResponseValidator(spec)
response = MockResponse(data=json.dumps(response))

result = validator.validate(request, response)
print(result.errors)
assert not result.errors

@pytest.mark.parametrize("response", post_object)
@pytest.mark.parametrize("spec_path", spec_paths)
def test_get_object_failure(self, factory, response, spec_path):
spec_dict = factory.spec_from_file(spec_path)
spec = create_spec(spec_dict)
request = MockRequest("http://www.example.com", "get",
"/object/{objectId}")

validator = ResponseValidator(spec)
response = MockResponse(data=json.dumps(response))

result = validator.validate(request, response)
assert result.errors
Loading