Skip to content

Yet another readOnly and writeOnly support #152

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

Merged
merged 6 commits into from
Feb 17, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion openapi_core/schema/schemas/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,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)

extensions = self.extensions_generator.generate(schema_deref)

Expand Down Expand Up @@ -81,7 +83,7 @@ def create(self, schema_spec):
exclusive_maximum=exclusive_maximum,
exclusive_minimum=exclusive_minimum,
min_properties=min_properties, max_properties=max_properties,
extensions=extensions,
read_only=read_only, write_only=write_only, extensions=extensions,
_source=schema_deref,
)

Expand Down
5 changes: 4 additions & 1 deletion openapi_core/schema/schemas/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,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, extensions=None,
min_properties=None, max_properties=None,
read_only=False, write_only=False, extensions=None,
_source=None):
self.type = SchemaType(schema_type)
self.properties = properties and dict(properties) or {}
Expand Down Expand Up @@ -56,6 +57,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

self.extensions = extensions and dict(extensions) or {}

Expand Down
29 changes: 29 additions & 0 deletions openapi_core/schema_validator/_validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,19 @@ def nullable(validator, is_nullable, instance, schema):
yield ValidationError("None for not nullable")


def required(validator, required, instance, schema):
if not validator.is_type(instance, "object"):
return
for property in required:
if property not in instance:
prop_schema = schema['properties'][property]
read_only = prop_schema.get('readOnly', False)
write_only = prop_schema.get('writeOnly', False)
if validator.write and read_only or validator.read and write_only:
continue
yield ValidationError("%r is a required property" % property)


def additionalProperties(validator, aP, instance, schema):
if not validator.is_type(instance, "object"):
return
Expand All @@ -54,5 +67,21 @@ def additionalProperties(validator, aP, instance, schema):
yield ValidationError(error % extras_msg(extras))


def readOnly(validator, ro, instance, schema):
if not validator.write or not ro:
return

yield ValidationError(
"Tried to write read-only proparty with %s" % (instance))


def writeOnly(validator, wo, instance, schema):
if not validator.read or not wo:
return

yield ValidationError(
"Tried to read write-only proparty with %s" % (instance))


def not_implemented(validator, value, instance, schema):
pass
11 changes: 8 additions & 3 deletions openapi_core/schema_validator/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
u"uniqueItems": _validators.uniqueItems,
u"maxProperties": _validators.maxProperties,
u"minProperties": _validators.minProperties,
u"required": _validators.required,
u"enum": _validators.enum,
# adjusted to OAS
u"type": oas_validators.type,
Expand All @@ -31,6 +30,7 @@
u"not": _validators.not_,
u"items": oas_validators.items,
u"properties": _validators.properties,
u"required": oas_validators.required,
u"additionalProperties": oas_validators.additionalProperties,
# TODO: adjust description
u"format": oas_validators.format,
Expand All @@ -39,8 +39,8 @@
# fixed OAS fields
u"nullable": oas_validators.nullable,
u"discriminator": oas_validators.not_implemented,
u"readOnly": oas_validators.not_implemented,
u"writeOnly": oas_validators.not_implemented,
u"readOnly": oas_validators.readOnly,
u"writeOnly": oas_validators.writeOnly,
u"xml": oas_validators.not_implemented,
u"externalDocs": oas_validators.not_implemented,
u"example": oas_validators.not_implemented,
Expand All @@ -54,6 +54,11 @@

class OAS30Validator(BaseOAS30Validator):

def __init__(self, *args, **kwargs):
self.read = kwargs.pop('read', None)
self.write = kwargs.pop('write', None)
super(OAS30Validator, self).__init__(*args, **kwargs)

def iter_errors(self, instance, _schema=None):
if _schema is None:
_schema = self.schema
Expand Down
7 changes: 7 additions & 0 deletions openapi_core/unmarshalling/schemas/enums.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
"""OpenAPI core unmarshalling schemas enums module"""
from enum import Enum


class UnmarshalContext(Enum):
REQUEST = 'request'
RESPONSE = 'response'
32 changes: 24 additions & 8 deletions openapi_core/unmarshalling/schemas/factories.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
from openapi_core.schema.schemas.models import Schema
from openapi_core.schema_validator import OAS30Validator
from openapi_core.schema_validator import oas30_format_checker
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
FormatterNotFoundError,
)
Expand All @@ -29,11 +30,17 @@ class SchemaUnmarshallersFactory(object):
SchemaType.ANY: AnyUnmarshaller,
}

def __init__(self, resolver=None, custom_formatters=None):
CONTEXT_VALIDATION = {
UnmarshalContext.REQUEST: 'write',
UnmarshalContext.RESPONSE: 'read',
}

def __init__(self, resolver=None, custom_formatters=None, context=None):
self.resolver = resolver
if custom_formatters is None:
custom_formatters = {}
self.custom_formatters = custom_formatters
self.context = context

def create(self, schema, type_override=None):
"""Create unmarshaller from the schema."""
Expand All @@ -50,7 +57,9 @@ def create(self, schema, type_override=None):
elif schema_type in self.COMPLEX_UNMARSHALLERS:
klass = self.COMPLEX_UNMARSHALLERS[schema_type]
kwargs = dict(
schema=schema, unmarshallers_factory=self)
schema=schema, unmarshallers_factory=self,
context=self.context,
)

formatter = self.get_formatter(klass.FORMATTERS, schema.format)

Expand All @@ -70,10 +79,17 @@ def get_formatter(self, default_formatters, type_format=SchemaFormat.NONE):
return default_formatters.get(schema_format)

def get_validator(self, schema):
format_checker = deepcopy(oas30_format_checker)
format_checker = self._get_format_checker()
kwargs = {
'resolver': self.resolver,
'format_checker': format_checker,
}
if self.context is not None:
kwargs[self.CONTEXT_VALIDATION[self.context]] = True
return OAS30Validator(schema.__dict__, **kwargs)

def _get_format_checker(self):
fc = deepcopy(oas30_format_checker)
for name, formatter in self.custom_formatters.items():
format_checker.checks(name)(formatter.validate)
return OAS30Validator(
schema.__dict__,
resolver=self.resolver, format_checker=format_checker,
)
fc.checks(name)(formatter.validate)
return fc
10 changes: 9 additions & 1 deletion openapi_core/unmarshalling/schemas/unmarshallers.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
is_object, is_number, is_string,
)
from openapi_core.schema_validator._format import oas30_format_checker
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError, InvalidSchemaValue,
InvalidSchemaFormatValue,
Expand Down Expand Up @@ -120,9 +121,12 @@ class BooleanUnmarshaller(PrimitiveTypeUnmarshaller):

class ComplexUnmarshaller(PrimitiveTypeUnmarshaller):

def __init__(self, formatter, validator, schema, unmarshallers_factory):
def __init__(
self, formatter, validator, schema, unmarshallers_factory,
context=None):
super(ComplexUnmarshaller, self).__init__(formatter, validator, schema)
self.unmarshallers_factory = unmarshallers_factory
self.context = context


class ArrayUnmarshaller(ComplexUnmarshaller):
Expand Down Expand Up @@ -206,6 +210,10 @@ def _unmarshal_properties(self, value=NoValue, one_of_schema=None):
properties[prop_name] = prop_value

for prop_name, prop in iteritems(all_props):
if self.context == UnmarshalContext.REQUEST and prop.read_only:
continue
if self.context == UnmarshalContext.RESPONSE and prop.write_only:
continue
try:
prop_value = value[prop_name]
except KeyError:
Expand Down
5 changes: 4 additions & 1 deletion openapi_core/validation/request/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from openapi_core.schema.request_bodies.exceptions import MissingRequestBody
from openapi_core.schema.servers.exceptions import InvalidServer
from openapi_core.security.exceptions import SecurityError
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError,
)
Expand Down Expand Up @@ -256,7 +257,9 @@ def _unmarshal(self, param_or_media_type, value):
SchemaUnmarshallersFactory,
)
unmarshallers_factory = SchemaUnmarshallersFactory(
self.spec._resolver, self.custom_formatters)
self.spec._resolver, self.custom_formatters,
context=UnmarshalContext.REQUEST,
)
unmarshaller = unmarshallers_factory.create(
param_or_media_type.schema)
return unmarshaller(value)
5 changes: 4 additions & 1 deletion openapi_core/validation/response/validators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
InvalidResponse, MissingResponseContent,
)
from openapi_core.schema.servers.exceptions import InvalidServer
from openapi_core.unmarshalling.schemas.enums import UnmarshalContext
from openapi_core.unmarshalling.schemas.exceptions import (
UnmarshalError, ValidateError,
)
Expand Down Expand Up @@ -139,7 +140,9 @@ def _unmarshal(self, param_or_media_type, value):
SchemaUnmarshallersFactory,
)
unmarshallers_factory = SchemaUnmarshallersFactory(
self.spec._resolver, self.custom_formatters)
self.spec._resolver, self.custom_formatters,
context=UnmarshalContext.RESPONSE,
)
unmarshaller = unmarshallers_factory.create(
param_or_media_type.schema)
return unmarshaller(value)
39 changes: 39 additions & 0 deletions tests/integration/data/v3.0/read_only_write_only.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
openapi: "3.0.0"
info:
title: Specification Containing readOnly
version: "0.1"
paths:
/users:
post:
operationId: createUser
requestBody:
description: Post data for creating a user
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/User'
responses:
default:
description: Create a user
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
User:
type: object
required:
- id
- name
properties:
id:
type: integer
format: int32
readOnly: true
name:
type: string
hidden:
type: boolean
writeOnly: true
6 changes: 5 additions & 1 deletion tests/integration/schema/test_spec.py
Original file line number Diff line number Diff line change
Expand Up @@ -268,5 +268,9 @@ def test_spec(self, spec, spec_dict):
if not spec.components:
return

for _, schema in iteritems(spec.components.schemas):
for schema_name, schema in iteritems(spec.components.schemas):
assert type(schema) == Schema

schema_spec = spec_dict['components']['schemas'][schema_name]
assert schema.read_only == schema_spec.get('readOnly', False)
assert schema.write_only == schema_spec.get('writeOnly', False)
Loading