Skip to content

Commit

Permalink
implement convention to make use of ETag and If-Match optional
Browse files Browse the repository at this point in the history
  • Loading branch information
Martin Nally committed May 20, 2016
1 parent 0c8fa96 commit f91f10c
Show file tree
Hide file tree
Showing 3 changed files with 47 additions and 31 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -473,6 +473,7 @@ Field Name | Type | Description
queryPathSelectorLocation | `string` | Either the string "pathSegment" or "pathParameter". The default is "pathParameter". This controls whether the selector for a multi-valued relationship appears in a separate path segment of the URL, or as a path parameter in the same path segment as the relationship name.
patchConsumes | `string` | The media type used for PATCH requests. Default is `['application/merge-patch+json']`
errorResponse | `schema` | the schema of the response for all error cases. the default is `{}`
useEtag | `boolean` | Whether or not the ETag and If-Match headers are used to detect update collisions. default is True

#### <a name="entities"></a>Entities

Expand Down
70 changes: 40 additions & 30 deletions util/gen_openapispec.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def openAPI_spec_from_rapier(self, filename):
else:
self.relationship_separator = ';'
self.patch_consumes = as_list(self.conventions['patchConsumes']) if 'patchConsumes' in self.conventions else ['application/merge-patch+json']
self.use_etag = self.conventions.get('useEtag', True) is True
patterns = spec.get('patterns')
self.openapispec = PresortedOrderedDict()
if self.include_impl:
Expand Down Expand Up @@ -303,7 +304,6 @@ def build_response_200():
interface[update_verb] = {
'description': description,
'parameters': [
{'$ref': parameter_ref},
{'name': 'body',
'in': 'body',
'description': body_desciption,
Expand All @@ -314,6 +314,8 @@ def build_response_200():
'200': response_200 if self.yaml_merge else build_response_200()
}
}
if self.use_etag:
interface[update_verb]['parameters'].insert(0, {'$ref': parameter_ref})
if not self.yaml_merge:
interface[update_verb]['responses'].update(self.build_put_patch_responses())
else:
Expand Down Expand Up @@ -431,10 +433,6 @@ def build_relationship_interface(self, entity_url_spec, query_path, rel_property
'type': 'string',
'description': location_desciption
},
'ETag': {
'type': 'string',
'description': 'Value of ETag required for subsequent updates'
},
'Content-Type': {
'type': 'string',
'description': 'The media type of the returned body'
Expand All @@ -443,6 +441,11 @@ def build_relationship_interface(self, entity_url_spec, query_path, rel_property
}
}
}
if self.use_etag:
interface['post']['responses']['201']['headers']['ETag'] = {
'type': 'string',
'description': 'Value of ETag required for subsequent updates'
}
if self.include_impl and produces and len(produces)> 1:
interface['post']['responses']['201']['headers']['Vary'] = {
'type': 'string',
Expand Down Expand Up @@ -483,15 +486,17 @@ def build_entity_get_responses(self):
}

def build_put_patch_responses(self):
return {
rslt = {
'400': self.global_response_ref('400'),
'401': self.global_response_ref('401'),
'403': self.global_response_ref('403'),
'404': self.global_response_ref('404'),
'406': self.global_response_ref('406'),
'409': self.global_response_ref('409'),
'default': self.global_response_ref('default')
}
}
if self.use_etag:
rslt['409'] = self.global_response_ref('409')
return rslt

def build_delete_responses(self):
return {
Expand Down Expand Up @@ -569,16 +574,17 @@ def build_standard_200(self, produces=None):
'type': 'string',
'description': 'perma-link URL of resource'
},
'ETag': {
'description': 'this value must be echoed in the If-Match header of every PATCH or PUT',
'type': 'string'
},
'Content-Type': {
'type': 'string',
'description': 'The media type of the returned body'
}
}
}
if self.use_etag:
rslt['headers']['ETag'] = \
{'description': 'this value must be echoed in the If-Match header of every PATCH or PUT',
'type': 'string'
}
if self.include_impl and produces and len(produces)> 1:
rslt['headers']['Vary'] = {
'type': 'string',
Expand All @@ -588,7 +594,7 @@ def build_standard_200(self, produces=None):
return rslt

def build_standard_responses(self):
return {
rslt = {
'standard_200': self.build_standard_200(),
'options_200': {
'description': 'successful',
Expand Down Expand Up @@ -640,15 +646,17 @@ def build_standard_responses(self):
'description': 'Not Acceptable. Requested media type not available',
'schema': self.error_response if self.yaml_merge else self.error_response.copy()
},
'409': {
'description': 'Conflict. Value provided in If-Match header does not match current ETag value of resource',
'schema': self.error_response if self.yaml_merge else self.error_response.copy()
},
'default': {
'description': '5xx errors and other stuff',
'schema': self.error_response if self.yaml_merge else self.error_response.copy()
}
}
if self.use_etag:
rslt['409'] = \
{'description': 'Conflict. Value provided in If-Match header does not match current ETag value of resource',
'schema': self.error_response if self.yaml_merge else self.error_response.copy()
}
return rslt

def build_collection_get(self, rel_property_spec, produces):
collection_entity_uri = rel_property_spec.collection_resource
Expand Down Expand Up @@ -712,7 +720,7 @@ def add_query_parameters(entity, query_params):
return rslt

def define_put_if_match_header(self):
if not 'Put-If-Match' in self.header_parameters:
if self.use_etag and not 'Put-If-Match' in self.header_parameters:
self.header_parameters['Put-If-Match'] = {
'name': 'If-Match',
'in': 'header',
Expand All @@ -722,14 +730,7 @@ def define_put_if_match_header(self):
}

def build_standard_header_parameters(self):
return {
'If-Match': {
'name': 'If-Match',
'in': 'header',
'type': 'string',
'description': 'specifies the last known ETag value of the resource being modified',
'required': True
},
rslt = {
'Accept': {
'name': 'Accept',
'in': 'header',
Expand All @@ -752,6 +753,15 @@ def build_standard_header_parameters(self):
'type': 'string'
}
}
if self.use_etag:
rslt['If-Match'] = {
'name': 'If-Match',
'in': 'header',
'type': 'string',
'description': 'specifies the last known ETag value of the resource being modified',
'required': True
}
return rslt

def resolve_ref_uri(self, ref_uri):
if ref_uri.startswith('#/'):
Expand Down Expand Up @@ -1141,9 +1151,9 @@ class URITemplateSpec(PathPrefix):
OPERATOR = "+#./;?&|!@"

def __init__(self, uri_template, entity_uri, generator):
self.uri_template = uri_template
self.template_string = uri_template['template']
self.template_variables = uri_template.get('variables', {})
self.uri_template = uri_template if hasattr(uri_template, 'keys') else {'template': uri_template}
self.template_string = self.uri_template['template']
self.template_variables = self.uri_template.get('variables', {})
self.entity_uri = entity_uri
self.generator = generator
split = self.template_string.split('{?')
Expand Down Expand Up @@ -1195,7 +1205,7 @@ def build_parameters(self, query_path=None):
class ImplementationPathSpec(PathPrefix):

def __init__(self, permalink_template, entity_uri, generator):
self.permalink_template = permalink_template
self.permalink_template = permalink_template if hasattr(permalink_template, 'keys') else {'template': permalink_template}
self.entity_uri = entity_uri
self.generator = generator
template = self.permalink_template['template']
Expand Down
7 changes: 6 additions & 1 deletion util/validate_rapier.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,7 +204,11 @@ def validate_properties(self, node, key, properties):

def validate_readOnly(self, node, key, readOnly):
if not (readOnly is True or readOnly is False) :
self.error('id must be a boolean: %s' % readOnly, key)
self.error('readOnly must be a boolean: %s' % readOnly, key)

def validate_useEtag(self, node, key, readOnly):
if not (readOnly is True or readOnly is False) :
self.error('useEtag must be a boolean: %s' % readOnly, key)

def validate_entity_readOnly(self, node, key, readOnly):
if not (readOnly is True or readOnly is False) :
Expand Down Expand Up @@ -666,6 +670,7 @@ def validate_minItems(self, node, key, value):
conventions_keywords = {
'queryPathSelectorLocation': validate_conventions_queryPathSelectorLocation,
'patchConsumes': validate_conventions_patch_consumes,
'useEtag': validate_useEtag,
'errorResponse': validate_conventions_error_response}
relationship_keywords = {
'entities': validate_relationship_entities,
Expand Down

0 comments on commit f91f10c

Please sign in to comment.