diff --git a/marconiclient/common/__init__.py b/marconiclient/common/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/marconiclient/common/api.py b/marconiclient/common/api.py new file mode 100644 index 00000000..a38ebe9d --- /dev/null +++ b/marconiclient/common/api.py @@ -0,0 +1,67 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import collections + + +ApiInfo = collections.namedtuple('ApiInfo', 'mandatory optional') + +_API_DATA = dict( + create_queue=ApiInfo( + mandatory=set(['queue_name']), optional=set()), + list_queues=ApiInfo( + mandatory=set(), optional=set(['marker', 'limit', 'detailed'])), + queue_exists=ApiInfo(mandatory=set(['queue_name']), optional=set()), + delete_queue=ApiInfo(mandatory=set(['queue_name']), optional=set()), + set_queue_metadata=ApiInfo( + mandatory=set(['queue_name', 'metadata']), optional=set()), + get_queue_metadata=ApiInfo( + mandatory=set(['queue_name']), optional=set()), + get_queue_stats=ApiInfo(mandatory=set(['queue_name']), optional=set()), + list_messages=ApiInfo( + mandatory=set(['queue_name']), + optional=set(['marker', 'limit', 'echo', 'include_claimed'])), + get_message=ApiInfo( + mandatory=set(['queue_name', 'message_id']), + optional=set(['claim_id'])), + get_messages_by_id=ApiInfo( + mandatory=set(['queue_name', 'message_ids']), + optional=set()), + post_messages=ApiInfo( + mandatory=set(['queue_name', 'messagedata']), optional=set()), + delete_message=ApiInfo( + mandatory=set(['queue_name', 'message_id']), + optional=set(['claim_id'])), + delete_messages_by_id=ApiInfo( + mandatory=set(['queue_name', 'message_ids']), optional=set()), + claim_messages=ApiInfo( + mandatory=set(['queue_name', 'ttl', 'grace_period']), + optional=set(['limit'])), + query_claim=ApiInfo( + mandatory=set(['queue_name', 'claim_id']), optional=set()), + update_claim=ApiInfo( + mandatory=set(['queue_name', 'claim_id', 'ttl']), optional=set()), + release_claim=ApiInfo( + mandatory=set(['queue_name', 'claim_id']), optional=set()), +) + + +def info(): + """A dict where the keys and values are valid operations and `ApiInfo` + named tuples respectively. + The `ApiInfo` named tuples have a `mandatory` and an `optional` property + that list the params for the respective operation. + """ + return _API_DATA.copy() diff --git a/marconiclient/transport/__init__.py b/marconiclient/transport/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/marconiclient/transport/request.py b/marconiclient/transport/request.py new file mode 100644 index 00000000..9ec0395c --- /dev/null +++ b/marconiclient/transport/request.py @@ -0,0 +1,55 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + + +from marconiclient.common import api + + +class Request(object): + """General data for a Marconi request, passed to the transport layer. + The idea is to be declarative i.e. specify *what* is desired. It's up to + the respective transport to turn this into a layer-specific request. + """ + + def __init__(self, endpoint='', operation='', params=None, headers=None): + self.endpoint = endpoint + self.operation = operation + self.params = params or {} + self.headers = headers or {} + + def validate(self): + """`None` if the request data is valid, an error message otherwise. + Checks the `operation` and the presence of the `params`. + """ + api_info_data = api.info() + if self.operation not in api_info_data: + return "Invalid operation '%s'" % self.operation + + api_info = api_info_data[self.operation] + + param_names = set() if not self.params else set(self.params.keys()) + # NOTE(al-maisan) Do we have all the mandatory params? + if not api_info.mandatory.issubset(param_names): + missing = sorted(api_info.mandatory - param_names) + return "Missing mandatory params: '%s'" % ', '.join(missing) + + # NOTE(al-maisan) Our params must be either in the mandatory or the + # optional subset. + all_permissible_params = api_info.mandatory.union(api_info.optional) + if not param_names.issubset(all_permissible_params): + invalid = sorted(param_names - all_permissible_params) + return "Invalid params: '%s'" % ', '.join(invalid) + + return None diff --git a/tests/unit/transport/test_request.py b/tests/unit/transport/test_request.py new file mode 100644 index 00000000..d01044d1 --- /dev/null +++ b/tests/unit/transport/test_request.py @@ -0,0 +1,48 @@ +# Copyright (c) 2013 Rackspace, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or +# implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from marconiclient.tests import base +from marconiclient.transport import request + + +HREF = '/v1/queue/' + + +class TestRequest(base.TestBase): + def test_valid_operation(self): + req = request.Request(endpoint=HREF, operation='create_queue', + params=dict(queue_name='high_load')) + self.assertIs(None, req.validate()) + + def test_invalid_operation(self): + req = request.Request(endpoint=HREF, operation='jump_queue', + params=dict(name='high_load')) + self.assertEqual("Invalid operation 'jump_queue'", req.validate()) + + def test_missing_mandatory_param(self): + req = request.Request(endpoint=HREF, operation='get_message', + params=dict()) + self.assertEqual("Missing mandatory params: 'message_id, queue_name'", + req.validate()) + + def test_missing_optional_param(self): + req = request.Request(endpoint=HREF, operation='delete_message', + params=dict(queue_name='abc', message_id='1')) + self.assertIs(None, req.validate()) + + def test_invalid__param(self): + req = request.Request(endpoint=HREF, operation='delete_queue', + params=dict(queue_name='xy', WAT='!?')) + self.assertEqual("Invalid params: 'WAT'", req.validate())