diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0235885 --- /dev/null +++ b/.gitignore @@ -0,0 +1,8 @@ +.idea/ +**/__pycache__/ +**/build/ +**/dist/ +**/*.egg-info/ +**/.DS_Store +.vscode +.coverage \ No newline at end of file diff --git a/CONTRIBUTING.adoc b/CONTRIBUTING.adoc new file mode 100644 index 0000000..d5bb791 --- /dev/null +++ b/CONTRIBUTING.adoc @@ -0,0 +1,36 @@ += Contributing to Eclipse uProtocol + +Thanks for your interest in this project. Contributions are welcome! + +== Developer resources + +Information regarding source code management, builds, coding standards, and +more. + +https://projects.eclipse.org/proposals/eclipse-uprotocol + +The project maintains the following source code repositories + +* https://github.com/eclipse-uprotocol + +== Eclipse Contributor Agreement + +Before your contribution can be accepted by the project team contributors must +electronically sign the Eclipse Contributor Agreement (ECA). + +* http://www.eclipse.org/legal/ECA.php + +Commits that are provided by non-committers must have a Signed-off-by field in +the footer indicating that the author is aware of the terms by which the +contribution has been provided to the project. The non-committer must +additionally have an Eclipse Foundation account and must have a signed Eclipse +Contributor Agreement (ECA) on file. + +For more information, please see the Eclipse Committer Handbook: +https://www.eclipse.org/projects/handbook/#resources-commit + +== Contact + +Contact the project developers via the project's "dev" list. + +* https://accounts.eclipse.org/mailing-list/uprotocol-dev \ No newline at end of file diff --git a/CONTRIBUTORS.adoc b/CONTRIBUTORS.adoc new file mode 100644 index 0000000..90a86ab --- /dev/null +++ b/CONTRIBUTORS.adoc @@ -0,0 +1,10 @@ += Eclipse uProtocol Contributors + +These are the contributors to Eclipse uProtocol Python SDK + +|=== +| GitHub username | Name + +|https://github.com/neelam-kushwah[neelam-kushwah] |Neelam Kushwah + +|=== \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + 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. diff --git a/NOTICE.adoc b/NOTICE.adoc new file mode 100644 index 0000000..5fbea7b --- /dev/null +++ b/NOTICE.adoc @@ -0,0 +1,33 @@ += Notices for Eclipse uProtocol + +This content is produced and maintained by the Eclipse uProtocol project. + +* Project home: https://projects.eclipse.org/projects/automotive.uprotocol + +== Trademarks + +Eclipse uProtocol is trademark of the Eclipse Foundation. +Eclipse, and the Eclipse Logo are registered trademarks of the Eclipse Foundation. + +== Copyright + +All content is the property of the respective authors or their employers. +For more information regarding authorship of content, please consult the +listed source code repository logs. + +== Declared Project Licenses + +This program and the accompanying materials are made available under the +terms of the or the Apache License, Version 2.0 +which is available at https://www.apache.org/licenses/LICENSE-2.0. + +SPDX-License-Identifier: Apache-2.0 + +== Third-party Content + +The following are libraries that uProtocol project uses: + +* http://cloudevents.io +* http://protobuf.dev + +NOTE: Please refer to link:setup.cfg[] for more information of library dependencies \ No newline at end of file diff --git a/README.adoc b/README.adoc new file mode 100644 index 0000000..641a20c --- /dev/null +++ b/README.adoc @@ -0,0 +1,86 @@ += Eclipse uProtocol Python Library +:toc: + +== Overview + +This library implements the https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/languages.adoc[uProtocol Language Specific Library Requirements] for Python defined in https://github.com/eclipse-uprotocol/uprotocol-spec/tree/main[uProtocol Specifications]. The library is organized into packages that are described in <> below. Each package contains a README.adoc file that describes the purpose of the package and how to use it. + +The module contains the factory methods, serializers, and validators for all data types defined in the specifications, and any data models that either haven't or couldn't be defined in uprotocol-core-api yet (ex. UPayload) This library fits into the big picture of the uProtocol SDK as seen in <> below. + +.uProtocol SDK +image:https://raw.githubusercontent.com/eclipse-uprotocol/uprotocol-spec/main/uprotocol_sdk.drawio.svg[#uprotocol-sdk,width=100%,align="center"] + + +== Getting Started + +=== Importing the sdk + +Setup SDK local repository and install +[source] +---- +$ git clone https://github.com/eclipse-uprotocol/uprotocol-python.git +$ cd uprotocol-python +$ pip install . +---- +*This will install the uprotocol-python, making its classes and modules available for import in your python code.* + + +=== Using The Sdk + +The SDK is broken up into different packages that are described in <> below. Each package contains a README.adoc file that describes the purpose of the package and how to use it. Packages are organized into the following directories: + +.Package Folders +[#pkg-folders,width=100%,cols="20%,80%",options="header"] +|=== + +| Folder | Purpose + +| `*builder*` or `*factory*` +| Contains factory methods for creating uProtocol data types + +| `*serializer*` +| Contains serializers to convert the objects into byte or string form representation of said object + +| `*validator*` +| Contains validators to validate the data types and report errors if the objects are missing or incorrect + +|=== + + +.SDK Packages +[#sdk-packages,width=100%,cols="20%,80%",options="header"] +|=== + +| Package | Purpose + +| link:uprotocol/uri/README.adoc[`*uuri*`] +| Uniform Resource Identifier (RFC3986), how uProtocol addresses things (devices, software, methods, topics, etc...) on the network + +| link:uprotocol/uuid/README.adoc[`*uuid*`] +| Identifier used to uniquely identify (and timestamp) messages that are sent + +| link:uprotocol/rpc/README.adoc[`*rpc*`] +| Interface to build client and service stubs for uServices. This interface is then implemented by https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/up-l2/rpcclient.adoc[RpcClient interface] + +| link:uprotocol/transport/README.adoc[`*utransport*`] +| Interface and data model declaration used for bidirectional point-2-point communication between uEs. This interface is then implemented by https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/ulink.adoc[ulink] libraries for a given underlining transport (ex. Binder, MQTT, Zenoh, SOME/IP, DDS, HTTP, etc…​) + +| link:uprotocol/cloudevent/README.adoc[`*cloudevent*`] +| Common way to represent uProtocol messages using CloudEvent data model used by some transports (ex. MQTT, HTTP, etc…​) + +|=== + +NOTE: Please visit the READMEs in <> for examples of how to use the different data types and their factories, validators, and serializers. + + +=== Cleaning Up + +Clean up by running the command: +`python clean_project.py` + +=== Running the Tests + +Requires coverage to be installed first, that can be done by running `pip install coverage` + +then you run: +`python -m coverage run --source tests/ -m unittest discover` diff --git a/README.md b/README.md deleted file mode 100644 index fbf0373..0000000 --- a/README.md +++ /dev/null @@ -1,2 +0,0 @@ -# uprotocol-sdk-python -uProtocol Python SDK diff --git a/clean_project.py b/clean_project.py new file mode 100644 index 0000000..8a2ae69 --- /dev/null +++ b/clean_project.py @@ -0,0 +1,22 @@ +import os +import shutil + + +def clean_project(): + # Remove build/ directory + if os.path.exists('build'): + shutil.rmtree('build') + + # Remove dist/ directory + if os.path.exists('dist'): + shutil.rmtree('dist') + + # Remove *.egg-info/ directories + egg_info_directories = [d for d in os.listdir() if d.endswith('.egg-info')] + for egg_info_directory in egg_info_directories: + shutil.rmtree(egg_info_directory) + + +if __name__ == "__main__": + clean_project() + print("Cleanup complete.") diff --git a/setup.cfg b/setup.cfg new file mode 100644 index 0000000..1240532 --- /dev/null +++ b/setup.cfg @@ -0,0 +1,19 @@ +[metadata] +name = uprotocol-python +author = Neelam Kushwah +author_email = neelam.kushwah@gm.com +description = UProtocol Python SDK +version = 1 +keywords = + uprotocol + python + +[options] +python_requires = >= 3.8 +packages = find: +zip_safe = False +install_requires = + cloudevents +; googleapis-common-protos>=1.56.4 + + diff --git a/setup.py b/setup.py new file mode 100644 index 0000000..6b40b52 --- /dev/null +++ b/setup.py @@ -0,0 +1,4 @@ +from setuptools import setup + +if __name__ == '__main__': + setup() diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cloudevent/__init__.py b/tests/test_cloudevent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cloudevent/test_datamodel/__init__.py b/tests/test_cloudevent/test_datamodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cloudevent/test_datamodel/test_ucloudeventattributes.py b/tests/test_cloudevent/test_datamodel/test_ucloudeventattributes.py new file mode 100644 index 0000000..1c16de9 --- /dev/null +++ b/tests/test_cloudevent/test_datamodel/test_ucloudeventattributes.py @@ -0,0 +1,86 @@ + +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + +import unittest + +from uprotocol.cloudevent.datamodel.ucloudeventattributes import UCloudEventAttributesBuilder, \ + UCloudEventAttributes +from uprotocol.proto.uattributes_pb2 import UPriority + + +class TestUCloudEventAttributes(unittest.TestCase): + + def test_to_string(self): + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).with_token("someOAuthToken").build() + + expected = "UCloudEventAttributes{hash='somehash', priority=UPRIORITY_CS1, ttl=3, token='someOAuthToken'}" + self.assertEqual(expected, str(u_cloud_event_attributes)) + + def test_create_valid(self): + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS6).with_ttl(3).with_token("someOAuthToken").build() + + self.assertEqual("somehash", u_cloud_event_attributes.get_hash()) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS6), u_cloud_event_attributes.get_priority()) + self.assertEqual(3, u_cloud_event_attributes.get_ttl()) + self.assertEqual("someOAuthToken", u_cloud_event_attributes.get_token()) + + def test_is_empty_function(self): + u_cloud_event_attributes = UCloudEventAttributes.empty() + self.assertTrue(u_cloud_event_attributes.is_empty()) + self.assertTrue(u_cloud_event_attributes.priority is None) + self.assertTrue(u_cloud_event_attributes.token is None) + self.assertTrue(u_cloud_event_attributes.ttl is None) + + def test_is_empty_function_when_built_with_blank_strings(self): + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build() + self.assertTrue(u_cloud_event_attributes.is_empty()) + self.assertTrue(u_cloud_event_attributes.hash.isspace()) + self.assertTrue(u_cloud_event_attributes.priority is None) + self.assertTrue(u_cloud_event_attributes.token.isspace()) + self.assertTrue(u_cloud_event_attributes.ttl is None) + + def test_is_empty_function_permutations(self): + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash(" ").with_token(" ").build() + self.assertTrue(u_cloud_event_attributes.is_empty()) + + u_cloud_event_attributes2 = UCloudEventAttributesBuilder().with_hash("someHash").with_token(" ").build() + self.assertFalse(u_cloud_event_attributes2.is_empty()) + + u_cloud_event_attributes3 = UCloudEventAttributesBuilder().with_hash(" ").with_token("SomeToken").build() + self.assertFalse(u_cloud_event_attributes3.is_empty()) + + u_cloud_event_attributes4 = UCloudEventAttributesBuilder().with_priority(UPriority.UPRIORITY_CS0).build() + self.assertFalse(u_cloud_event_attributes4.is_empty()) + + u_cloud_event_attributes5 = UCloudEventAttributesBuilder().with_ttl(8).build() + self.assertFalse(u_cloud_event_attributes5.is_empty()) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_cloudevent/test_factory/__init__.py b/tests/test_cloudevent/test_factory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cloudevent/test_factory/cloudevent.json b/tests/test_cloudevent/test_factory/cloudevent.json new file mode 100644 index 0000000..a11e728 --- /dev/null +++ b/tests/test_cloudevent/test_factory/cloudevent.json @@ -0,0 +1,99 @@ +[ + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoidGVzdG1lIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJwdWIudjEiLCJwcmlvcml0eSI6IlVQUklPUklUWV9DUzEiLCJ0dGwiOjMsImhhc2giOiJzb21laGFzaCIsInRva2VuIjoic29tZU9BdXRoVG9rZW4iLCJkYXRhX2Jhc2U2NCI6IkNqQjBlWEJsTG1kdmIyZHNaV0Z3YVhNdVkyOXRMMmx2TG1Oc2IzVmtaWFpsYm5SekxuWXhMa05zYjNWa1JYWmxiblFTTVFvRmFHVnNiRzhTRTJoMGRIQnpPaTh2WlhoaGJYQnNaUzVqYjIwYUF6RXVNQ0lNWlhoaGJYQnNaUzVrWlcxdlFnQT0ifQ==", + "id": "testme", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS1", + "ttl": 3, + "hash": "somehash", + "token": "someOAuthToken", + "test_detail": "test_create_base_cloud_event" + }, + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoidGVzdG1lIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJwdWIudjEiLCJkYXRhY29udGVudHR5cGUiOiJhcHBsaWNhdGlvbi94LXByb3RvYnVmIiwiZGF0YXNjaGVtYSI6InR5cGUuZ29vZ2xlYXBpcy5jb20vaW8uY2xvdWRldmVudHMudjEuQ2xvdWRFdmVudCIsInByaW9yaXR5IjoiVVBSSU9SSVRZX0NTMSIsInR0bCI6MywiaGFzaCI6InNvbWVoYXNoIiwidG9rZW4iOiJzb21lT0F1dGhUb2tlbiIsImRhdGFfYmFzZTY0IjoiQ2pCMGVYQmxMbWR2YjJkc1pXRndhWE11WTI5dEwybHZMbU5zYjNWa1pYWmxiblJ6TG5ZeExrTnNiM1ZrUlhabGJuUVNNUW9GYUdWc2JHOFNFMmgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjBhQXpFdU1DSU1aWGhoYlhCc1pTNWtaVzF2UWdBPSJ9", + "id": "testme", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS1", + "ttl": 3, + "hash": "somehash", + "token": "someOAuthToken", + "dataschema": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + "datacontenttype": "application/x-protobuf", + "test_detail": "test_create_base_cloud_event_with_datacontenttype_and_schema" + }, + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoidGVzdG1lIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJwdWIudjEiLCJkYXRhX2Jhc2U2NCI6IkNqQjBlWEJsTG1kdmIyZHNaV0Z3YVhNdVkyOXRMMmx2TG1Oc2IzVmtaWFpsYm5SekxuWXhMa05zYjNWa1JYWmxiblFTTVFvRmFHVnNiRzhTRTJoMGRIQnpPaTh2WlhoaGJYQnNaUzVqYjIwYUF6RXVNQ0lNWlhoaGJYQnNaUzVrWlcxdlFnQT0ifQ==", + "id": "testme", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "test_detail": "test_create_base_cloud_event_without_attributes" + }, + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoiMDE4YmY5YjYtNWMxYy04MDAwLTk4MmYtOGM3MTI1NjQ2OWNjIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJwdWIudjEiLCJwcmlvcml0eSI6IlVQUklPUklUWV9DUzEiLCJ0dGwiOjMsImhhc2giOiJzb21laGFzaCIsImRhdGFfYmFzZTY0IjoiQ2pCMGVYQmxMbWR2YjJkc1pXRndhWE11WTI5dEwybHZMbU5zYjNWa1pYWmxiblJ6TG5ZeExrTnNiM1ZrUlhabGJuUVNNUW9GYUdWc2JHOFNFMmgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjBhQXpFdU1DSU1aWGhoYlhCc1pTNWtaVzF2UWdBPSJ9", + "id": "018bf9b6-5c1c-8000-982f-8c71256469cc", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS1", + "ttl": 3, + "hash": "somehash", + "test_detail": "test_create_publish_cloud_event" + }, + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoiMDE4YmY5YjktNWFkOS04MDAwLTgzZTUtNmE0ODQxNzNkNjlmIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJwdWIudjEiLCJzaW5rIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInByaW9yaXR5IjoiVVBSSU9SSVRZX0NTMiIsInR0bCI6MywiaGFzaCI6InNvbWVoYXNoIiwiZGF0YV9iYXNlNjQiOiJDakIwZVhCbExtZHZiMmRzWldGd2FYTXVZMjl0TDJsdkxtTnNiM1ZrWlhabGJuUnpMbll4TGtOc2IzVmtSWFpsYm5RU01Rb0ZhR1ZzYkc4U0UyaDBkSEJ6T2k4dlpYaGhiWEJzWlM1amIyMGFBekV1TUNJTVpYaGhiWEJzWlM1a1pXMXZRZ0E9In0=", + "id": "018bf9b9-5ad9-8000-83e5-6a484173d69f", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS2", + "ttl": 3, + "hash": "somehash", + "sink": "/body.access//door.front_left#Door", + "test_detail": "test_create_notification_cloud_event" + }, + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoiMDE4YmY5YmItMzFiZC04MDAwLWJkMGEtNjViNjNlZWNlMmJkIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJyZXEudjEiLCJzaW5rIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInByaW9yaXR5IjoiVVBSSU9SSVRZX0NTMiIsInR0bCI6MywiaGFzaCI6InNvbWVoYXNoIiwidG9rZW4iOiJzb21lT0F1dGhUb2tlbiIsImRhdGFfYmFzZTY0IjoiQ2pCMGVYQmxMbWR2YjJkc1pXRndhWE11WTI5dEwybHZMbU5zYjNWa1pYWmxiblJ6TG5ZeExrTnNiM1ZrUlhabGJuUVNNUW9GYUdWc2JHOFNFMmgwZEhCek9pOHZaWGhoYlhCc1pTNWpiMjBhQXpFdU1DSU1aWGhoYlhCc1pTNWtaVzF2UWdBPSJ9", + "id": "018bf9bb-31bd-8000-bd0a-65b63eece2bd", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "req.v1", + "priority": "UPRIORITY_CS2", + "ttl": 3, + "hash": "somehash", + "token": "someOAuthToken", + "test_detail": "test_create_request_cloud_event_from_local_use" + }, + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoiMDE4YmY5YmQtM2EzYi04MDAwLWE4Y2ItZTk1NTY4MWU4NTMwIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJyZXMudjEiLCJzaW5rIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInByaW9yaXR5IjoiVVBSSU9SSVRZX0NTMiIsInR0bCI6MywiaGFzaCI6InNvbWVoYXNoIiwicmVxaWQiOiJyZXF1ZXN0SWRGcm9tUmVxdWVzdENsb3VkRXZlbnQiLCJkYXRhX2Jhc2U2NCI6IkNqQjBlWEJsTG1kdmIyZHNaV0Z3YVhNdVkyOXRMMmx2TG1Oc2IzVmtaWFpsYm5SekxuWXhMa05zYjNWa1JYWmxiblFTTVFvRmFHVnNiRzhTRTJoMGRIQnpPaTh2WlhoaGJYQnNaUzVqYjIwYUF6RXVNQ0lNWlhoaGJYQnNaUzVrWlcxdlFnQT0ifQ==", + "id": "018bf9bd-3a3b-8000-a8cb-e955681e8530", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "res.v1", + "priority": "UPRIORITY_CS2", + "ttl": 3, + "hash": "somehash", + "reqid": "requestIdFromRequestCloudEvent", + "sink": "/body.access//door.front_left#Door", + "test_detail": "test_create_response_cloud_event_originating_from_local_use" + }, + { + "serialized_ce": "eyJzcGVjdmVyc2lvbiI6IjEuMCIsImlkIjoiMDE4YmY5YmYtMjdlNC04MDAwLThkZDAtMjYyNzdjNzg3OTBlIiwic291cmNlIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsInR5cGUiOiJyZXMudjEiLCJzaW5rIjoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vciIsImNvbW1zdGF0dXMiOjMsInByaW9yaXR5IjoiVVBSSU9SSVRZX0NTMiIsInR0bCI6MywiaGFzaCI6InNvbWVoYXNoIiwicmVxaWQiOiJyZXF1ZXN0SWRGcm9tUmVxdWVzdENsb3VkRXZlbnQiLCJkYXRhX2Jhc2U2NCI6IkNpbDBlWEJsTG1kdmIyZHNaV0Z3YVhNdVkyOXRMMmR2YjJkc1pTNXdjbTkwYjJKMVppNUZiWEIwZVE9PSJ9", + "id": "018bf9bf-27e4-8000-8dd0-26277c78790e", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "res.v1", + "priority": "UPRIORITY_CS2", + "ttl": 3, + "hash": "somehash", + "reqid": "requestIdFromRequestCloudEvent", + "sink": "/body.access//door.front_left#Door", + "commstatus": 3, + "test_detail": "test_create_a_failed_response_cloud_event" + } + +] \ No newline at end of file diff --git a/tests/test_cloudevent/test_factory/test_cloudeventfactory.py b/tests/test_cloudevent/test_factory/test_cloudeventfactory.py new file mode 100644 index 0000000..a4b1e2c --- /dev/null +++ b/tests/test_cloudevent/test_factory/test_cloudeventfactory.py @@ -0,0 +1,389 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import unittest +import json +import os +from google.protobuf import any_pb2 +from uprotocol.cloudevent.datamodel.ucloudeventattributes import UCloudEventAttributesBuilder, \ + UCloudEventAttributes +from uprotocol.cloudevent.factory.cloudeventfactory import CloudEventFactory +from uprotocol.cloudevent.factory.ucloudevent import UCloudEvent +from uprotocol.cloudevent.serialize.base64protobufserializer import Base64ProtobufSerializer +from uprotocol.cloudevent.serialize.cloudeventtojsonserializer import CloudEventToJsonSerializer +from uprotocol.proto.cloudevents_pb2 import CloudEvent +from uprotocol.proto.uattributes_pb2 import UMessageType, UPriority +from uprotocol.proto.uri_pb2 import UUri, UEntity, UResource +from uprotocol.proto.ustatus_pb2 import UCode + +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer + + +def get_json_object(): + current_directory = os.getcwd() + json_file_path = os.path.join(current_directory, "tests","test_cloudevent","test_factory","cloudevent.json") + + with open(json_file_path, 'r') as json_file: + json_data = json.load(json_file) + + return json_data + + +def build_uri_for_test(): + uri = UUri(entity=UEntity(name="body.access"), + resource=UResource(name="door", instance="front_left", message="Door")) + return LongUriSerializer().serialize(uri) + + +def build_proto_payload_for_test(): + ce_proto = CloudEvent(spec_version="1.0", source="https://example.com", id="hello", type="example.demo", + proto_data=any_pb2.Any()) + + any_obj = any_pb2.Any() + any_obj.Pack(ce_proto) + return any_obj + + +class TestCloudEventFactory(unittest.TestCase): + DATA_CONTENT_TYPE = CloudEventFactory.PROTOBUF_CONTENT_TYPE + + def test_all_cloud_events_from_json(self): + cloudevents = get_json_object() + for ce_json in cloudevents: + bytes_ce = Base64ProtobufSerializer().serialize(ce_json['serialized_ce']) + cloudevent = CloudEventToJsonSerializer().deserialize(bytes_ce) + self.assertEqual(UCloudEvent.get_id(cloudevent), ce_json['id']) + self.assertEqual(UCloudEvent.get_specversion(cloudevent), ce_json['specversion']) + if 'source' in ce_json: + self.assertEqual(UCloudEvent.get_source(cloudevent), ce_json['source']) + if 'sink' in ce_json: + self.assertEqual(UCloudEvent.get_sink(cloudevent), ce_json['sink']) + if 'type' in ce_json: + self.assertEqual(UCloudEvent.get_type(cloudevent), ce_json['type']) + if 'priority' in ce_json: + self.assertEqual(UCloudEvent.get_priority(cloudevent), ce_json['priority']) + if 'ttl' in ce_json: + self.assertEqual(UCloudEvent.get_ttl(cloudevent), ce_json['ttl']) + if 'hash' in ce_json: + self.assertEqual(UCloudEvent.get_hash(cloudevent), ce_json['hash']) + if 'token' in ce_json: + self.assertEqual(UCloudEvent.get_token(cloudevent), ce_json['token']) + if 'dataschema' in ce_json: + self.assertEqual(UCloudEvent.get_data_schema(cloudevent), ce_json['dataschema']) + if 'datacontenttype' in ce_json: + self.assertEqual(UCloudEvent.get_data_content_type(cloudevent), ce_json['datacontenttype']) + if 'commstatus' in ce_json: + self.assertEqual(UCloudEvent.get_communication_status(cloudevent), ce_json['commstatus']) + + def test_create_base_cloud_event(self): + source = build_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).with_token("someOAuthToken").build() + + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("testme", source, proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertEqual("testme", UCloudEvent.get_id(cloud_event)) + self.assertEqual(source, UCloudEvent.get_source(cloud_event)) + self.assertEqual('pub.v1', UCloudEvent.get_type(cloud_event)) + self.assertNotIn("sink", cloud_event.get_attributes()) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS1), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + self.assertEqual("someOAuthToken", UCloudEvent.get_token(cloud_event)) + self.assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + + def test_create_base_cloud_event_with_datacontenttype_and_schema(self): + source = build_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).with_token("someOAuthToken").build() + + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("testme", source, proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + + cloud_event.__setitem__('datacontenttype', CloudEventFactory.PROTOBUF_CONTENT_TYPE) + cloud_event.__setitem__('dataschema', proto_payload.type_url) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertEqual("testme", UCloudEvent.get_id(cloud_event)) + self.assertEqual(source, UCloudEvent.get_source(cloud_event)) + self.assertEqual(UCloudEvent.get_event_type(UMessageType.UMESSAGE_TYPE_PUBLISH), + UCloudEvent.get_type(cloud_event)) + self.assertEqual(self.DATA_CONTENT_TYPE, UCloudEvent.get_data_content_type(cloud_event)) + self.assertEqual(proto_payload.type_url, UCloudEvent.get_data_schema(cloud_event)) + self.assertNotIn("sink", cloud_event.get_attributes()) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS1), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + self.assertEqual("someOAuthToken", UCloudEvent.get_token(cloud_event)) + self.assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + + def test_create_base_cloud_event_without_attributes(self): + source = build_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + + # no additional attributes + u_cloud_event_attributes = UCloudEventAttributes.empty() + + # build the cloud event + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("testme", source, proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertEqual("testme", UCloudEvent.get_id(cloud_event)) + self.assertEqual(source, UCloudEvent.get_source(cloud_event)) + self.assertEqual(UCloudEvent.get_event_type(UMessageType.UMESSAGE_TYPE_PUBLISH), + UCloudEvent.get_type(cloud_event)) + self.assertNotIn("sink", cloud_event.get_attributes()) + self.assertNotIn("hash", cloud_event.get_attributes()) + self.assertNotIn("priority", cloud_event.get_attributes()) + self.assertNotIn("ttl", cloud_event.get_attributes()) + self.assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + + def test_create_publish_cloud_event(self): + # source + source = build_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).build() + + cloud_event = CloudEventFactory.publish(source, proto_payload, u_cloud_event_attributes) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertIsNotNone(UCloudEvent.get_id(cloud_event)) + self.assertEqual(source, UCloudEvent.get_source(cloud_event)) + self.assertEqual(UCloudEvent.get_event_type(UMessageType.UMESSAGE_TYPE_PUBLISH), + UCloudEvent.get_type(cloud_event)) + self.assertNotIn("sink", cloud_event.get_attributes()) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS1), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + + self.assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + + def test_create_notification_cloud_event(self): + # source + source = build_uri_for_test() + + # sink + sink = build_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + + # additional attributes + + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS2).with_ttl(3).build() + + # build the cloud event of type publish with destination - a notification + cloud_event = CloudEventFactory.notification(source, sink, proto_payload, u_cloud_event_attributes) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertIsNotNone(UCloudEvent.get_id(cloud_event)) + self.assertEqual(source, UCloudEvent.get_source(cloud_event)) + + self.assertIn("sink", cloud_event.get_attributes()) + self.assertEqual(sink, UCloudEvent.get_sink(cloud_event)) + + self.assertEqual(UCloudEvent.get_event_type(UMessageType.UMESSAGE_TYPE_PUBLISH), + UCloudEvent.get_type(cloud_event)) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS2), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + + self.assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + + def test_create_request_cloud_event_from_local_use(self): + # UriPart for the application requesting the RPC + application_uri_for_rpc = build_uri_for_test() + + # service Method UriPart + service_method_uri = build_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + + # additional attributes + + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS2).with_ttl(3).with_token("someOAuthToken").build() + + cloud_event = CloudEventFactory.request(application_uri_for_rpc, service_method_uri, + CloudEventFactory.generate_cloud_event_id(), proto_payload, + u_cloud_event_attributes) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertIsNotNone(UCloudEvent.get_id(cloud_event)) + self.assertEqual(application_uri_for_rpc, UCloudEvent.get_source(cloud_event)) + + self.assertIn("sink", cloud_event.get_attributes()) + self.assertEqual(service_method_uri, UCloudEvent.get_sink(cloud_event)) + + self.assertEqual("req.v1", UCloudEvent.get_type(cloud_event)) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS2), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + self.assertEqual("someOAuthToken", UCloudEvent.get_token(cloud_event)) + + self.assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + + def test_create_response_cloud_event_originating_from_local_use(self): + # UriPart for the application requesting the RPC + application_uri_for_rpc = build_uri_for_test() + + # service Method UriPart + service_method_uri = build_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + + # additional attributes + + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS2).with_ttl(3).build() + + cloud_event = CloudEventFactory.response(application_uri_for_rpc, service_method_uri, + "requestIdFromRequestCloudEvent", proto_payload, + u_cloud_event_attributes) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertIsNotNone(UCloudEvent.get_id(cloud_event)) + self.assertEqual(service_method_uri, UCloudEvent.get_source(cloud_event)) + + self.assertIn("sink", cloud_event.get_attributes()) + self.assertEqual(application_uri_for_rpc, UCloudEvent.get_sink(cloud_event)) + + self.assertEqual("res.v1", UCloudEvent.get_type(cloud_event)) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS2), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + + self.assertEqual("requestIdFromRequestCloudEvent", UCloudEvent.get_request_id(cloud_event)) + + # Use assertEqual to compare byte arrays + self.assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + + def test_create_failed_response_cloud_event_originating_from_local_use(self): + # UriPart for the application requesting the RPC + application_uri_for_rpc = build_uri_for_test() + + # service Method UriPart + service_method_uri = build_uri_for_test() + + # additional attributes + + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS2).with_ttl(3).build() + + cloud_event = CloudEventFactory.failed_response(application_uri_for_rpc, service_method_uri, + "requestIdFromRequestCloudEvent", UCode.INVALID_ARGUMENT, + u_cloud_event_attributes) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertIsNotNone(UCloudEvent.get_id(cloud_event)) + self.assertEqual(service_method_uri, UCloudEvent.get_source(cloud_event)) + + self.assertIn("sink", cloud_event.get_attributes()) + self.assertEqual(application_uri_for_rpc, UCloudEvent.get_sink(cloud_event)) + + self.assertEqual("res.v1", UCloudEvent.get_type(cloud_event)) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS2), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + self.assertEqual(UCode.INVALID_ARGUMENT, UCloudEvent.get_communication_status(cloud_event)) + + self.assertEqual("requestIdFromRequestCloudEvent", UCloudEvent.get_request_id(cloud_event)) + + def test_create_failed_response_cloud_event_originating_from_remote_use(self): + # UriPart for the application requesting the RPC + application_uri_for_rpc = build_uri_for_test() + + # service Method UriPart + service_method_uri = build_uri_for_test() + + # additional attributes + + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS2).with_ttl(3).build() + + cloud_event = CloudEventFactory.failed_response(application_uri_for_rpc, service_method_uri, + "requestIdFromRequestCloudEvent", UCode.INVALID_ARGUMENT, + u_cloud_event_attributes) + + # test all attributes + self.assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) + self.assertIsNotNone(UCloudEvent.get_id(cloud_event)) + self.assertEqual(service_method_uri, UCloudEvent.get_source(cloud_event)) + + self.assertIn("sink", cloud_event.get_attributes()) + self.assertEqual(application_uri_for_rpc, UCloudEvent.get_sink(cloud_event)) + + self.assertEqual("res.v1", UCloudEvent.get_type(cloud_event)) + self.assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) + self.assertEqual(UPriority.Name(UPriority.UPRIORITY_CS2), UCloudEvent.get_priority(cloud_event)) + self.assertEqual(3, UCloudEvent.get_ttl(cloud_event)) + self.assertEqual(UCode.INVALID_ARGUMENT, UCloudEvent.get_communication_status(cloud_event)) + + self.assertEqual("requestIdFromRequestCloudEvent", UCloudEvent.get_request_id(cloud_event)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_cloudevent/test_serialize/__init__.py b/tests/test_cloudevent/test_serialize/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cloudevent/test_serialize/cloudevent_to_protobuf.json b/tests/test_cloudevent/test_serialize/cloudevent_to_protobuf.json new file mode 100644 index 0000000..bd10a51 --- /dev/null +++ b/tests/test_cloudevent/test_serialize/cloudevent_to_protobuf.json @@ -0,0 +1,86 @@ +[ + { + "serialized_ce": "CgZ0ZXN0bWUSIi9ib2R5LmFjY2Vzcy8vZG9vci5mcm9udF9sZWZ0I0Rvb3IaAzEuMCIGcHViLnYxKhsKCHByaW9yaXR5Eg8aDVVQUklPUklUWV9DUzEqGQoFdG9rZW4SEBoOc29tZU9BdXRoVG9rZW4qKgoEdGltZRIiGiAyMDIzLTExLTI5VDE4OjE0OjQyLjQ2NDQwMSswMDowMCoSCgRoYXNoEgoaCHNvbWVoYXNoKgkKA3R0bBICEAMyZQowdHlwZS5nb29nbGVhcGlzLmNvbS9pby5jbG91ZGV2ZW50cy52MS5DbG91ZEV2ZW50EjEKBWhlbGxvEhNodHRwczovL2V4YW1wbGUuY29tGgMxLjAiDGV4YW1wbGUuZGVtb0IA", + "id": "testme", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS1", + "ttl": 3, + "hash": "somehash", + "token": "someOAuthToken", + "test_detail": "test_create_base_cloud_event" + }, + { + "serialized_ce": "CgZ0ZXN0bWUSIi9ib2R5LmFjY2Vzcy8vZG9vci5mcm9udF9sZWZ0I0Rvb3IaAzEuMCIGcHViLnYxKhsKCHByaW9yaXR5Eg8aDVVQUklPUklUWV9DUzEqEgoEaGFzaBIKGghzb21laGFzaCoJCgN0dGwSAhADKisKD2RhdGFjb250ZW50dHlwZRIYGhZhcHBsaWNhdGlvbi94LXByb3RvYnVmKhkKBXRva2VuEhAaDnNvbWVPQXV0aFRva2VuKioKBHRpbWUSIhogMjAyMy0xMS0yOVQxODoxNDo0Mi40NjUzOTYrMDA6MDAqQAoKZGF0YXNjaGVtYRIyGjB0eXBlLmdvb2dsZWFwaXMuY29tL2lvLmNsb3VkZXZlbnRzLnYxLkNsb3VkRXZlbnQyZQowdHlwZS5nb29nbGVhcGlzLmNvbS9pby5jbG91ZGV2ZW50cy52MS5DbG91ZEV2ZW50EjEKBWhlbGxvEhNodHRwczovL2V4YW1wbGUuY29tGgMxLjAiDGV4YW1wbGUuZGVtb0IA", + "id": "testme", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS1", + "ttl": 3, + "hash": "somehash", + "token": "someOAuthToken", + "dataschema": "type.googleapis.com/io.cloudevents.v1.CloudEvent", + "datacontenttype": "application/x-protobuf", + "test_detail": "test_create_base_cloud_event_with_datacontenttype_and_schema" + }, + { + "serialized_ce": "CgZ0ZXN0bWUSIi9ib2R5LmFjY2Vzcy8vZG9vci5mcm9udF9sZWZ0I0Rvb3IaAzEuMCIGcHViLnYxKioKBHRpbWUSIhogMjAyMy0xMS0yOVQxODoxNDo0Mi40NjcxMjQrMDA6MDAyZQowdHlwZS5nb29nbGVhcGlzLmNvbS9pby5jbG91ZGV2ZW50cy52MS5DbG91ZEV2ZW50EjEKBWhlbGxvEhNodHRwczovL2V4YW1wbGUuY29tGgMxLjAiDGV4YW1wbGUuZGVtb0IA", + "id": "testme", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "test_detail": "test_create_base_cloud_event_without_attributes" + }, + { + "serialized_ce": "CiQwMThiZjliNi01YzFjLTgwMDAtOTgyZi04YzcxMjU2NDY5Y2MSIi9ib2R5LmFjY2Vzcy8vZG9vci5mcm9udF9sZWZ0I0Rvb3IaAzEuMCIGcHViLnYxKhsKCHByaW9yaXR5Eg8aDVVQUklPUklUWV9DUzEqKgoEdGltZRIiGiAyMDIzLTExLTI5VDE4OjE0OjQyLjQ2NzEyNCswMDowMCoSCgRoYXNoEgoaCHNvbWVoYXNoKgkKA3R0bBICEAMyZQowdHlwZS5nb29nbGVhcGlzLmNvbS9pby5jbG91ZGV2ZW50cy52MS5DbG91ZEV2ZW50EjEKBWhlbGxvEhNodHRwczovL2V4YW1wbGUuY29tGgMxLjAiDGV4YW1wbGUuZGVtb0IA", + "id": "018bf9b6-5c1c-8000-982f-8c71256469cc", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS1", + "ttl": 3, + "hash": "somehash", + "test_detail": "test_create_publish_cloud_event" + }, + { + "serialized_ce": "CiQwMThiZjliOS01YWQ5LTgwMDAtODNlNS02YTQ4NDE3M2Q2OWYSIi9ib2R5LmFjY2Vzcy8vZG9vci5mcm9udF9sZWZ0I0Rvb3IaAzEuMCIGcHViLnYxKhsKCHByaW9yaXR5Eg8aDVVQUklPUklUWV9DUzIqLAoEc2luaxIkGiIvYm9keS5hY2Nlc3MvL2Rvb3IuZnJvbnRfbGVmdCNEb29yKioKBHRpbWUSIhogMjAyMy0xMS0yOVQxODoxNDo0Mi40NjcxMjQrMDA6MDAqEgoEaGFzaBIKGghzb21laGFzaCoJCgN0dGwSAhADMmUKMHR5cGUuZ29vZ2xlYXBpcy5jb20vaW8uY2xvdWRldmVudHMudjEuQ2xvdWRFdmVudBIxCgVoZWxsbxITaHR0cHM6Ly9leGFtcGxlLmNvbRoDMS4wIgxleGFtcGxlLmRlbW9CAA==", + "id": "018bf9b9-5ad9-8000-83e5-6a484173d69f", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "pub.v1", + "priority": "UPRIORITY_CS2", + "ttl": 3, + "hash": "somehash", + "sink": "/body.access//door.front_left#Door", + "test_detail": "test_create_notification_cloud_event" + }, + { + "serialized_ce": "CiQwMThiZjliZC0zYTNiLTgwMDAtYThjYi1lOTU1NjgxZTg1MzASIi9ib2R5LmFjY2Vzcy8vZG9vci5mcm9udF9sZWZ0I0Rvb3IaAzEuMCIGcmVzLnYxKhsKCHByaW9yaXR5Eg8aDVVQUklPUklUWV9DUzIqLAoEc2luaxIkGiIvYm9keS5hY2Nlc3MvL2Rvb3IuZnJvbnRfbGVmdCNEb29yKhIKBGhhc2gSChoIc29tZWhhc2gqKgoEdGltZRIiGiAyMDIzLTExLTI5VDE4OjE0OjQyLjQ2ODE0MSswMDowMCopCgVyZXFpZBIgGh5yZXF1ZXN0SWRGcm9tUmVxdWVzdENsb3VkRXZlbnQqCQoDdHRsEgIQAzJlCjB0eXBlLmdvb2dsZWFwaXMuY29tL2lvLmNsb3VkZXZlbnRzLnYxLkNsb3VkRXZlbnQSMQoFaGVsbG8SE2h0dHBzOi8vZXhhbXBsZS5jb20aAzEuMCIMZXhhbXBsZS5kZW1vQgA=", + "id": "018bf9bd-3a3b-8000-a8cb-e955681e8530", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "res.v1", + "priority": "UPRIORITY_CS2", + "ttl": 3, + "hash": "somehash", + "reqid": "requestIdFromRequestCloudEvent", + "sink": "/body.access//door.front_left#Door", + "test_detail": "test_create_response_cloud_event_originating_from_local_use" + }, + { + "serialized_ce": "CiQwMThiZjliZi0yN2U0LTgwMDAtOGRkMC0yNjI3N2M3ODc5MGUSIi9ib2R5LmFjY2Vzcy8vZG9vci5mcm9udF9sZWZ0I0Rvb3IaAzEuMCIGcmVzLnYxKhsKCHByaW9yaXR5Eg8aDVVQUklPUklUWV9DUzIqEgoEaGFzaBIKGghzb21laGFzaCoJCgN0dGwSAhADKhAKCmNvbW1zdGF0dXMSAhADKiwKBHNpbmsSJBoiL2JvZHkuYWNjZXNzLy9kb29yLmZyb250X2xlZnQjRG9vcioqCgR0aW1lEiIaIDIwMjMtMTEtMjlUMTg6MTQ6NDIuNDY4MTQxKzAwOjAwKikKBXJlcWlkEiAaHnJlcXVlc3RJZEZyb21SZXF1ZXN0Q2xvdWRFdmVudDIrCil0eXBlLmdvb2dsZWFwaXMuY29tL2dvb2dsZS5wcm90b2J1Zi5FbXB0eQ==", + "id": "018bf9bf-27e4-8000-8dd0-26277c78790e", + "specversion": "1.0", + "source": "/body.access//door.front_left#Door", + "type": "res.v1", + "priority": "UPRIORITY_CS2", + "ttl": 3, + "hash": "somehash", + "reqid": "requestIdFromRequestCloudEvent", + "sink": "/body.access//door.front_left#Door", + "commstatus": 3, + "test_detail": "test_create_a_failed_response_cloud_event" + } +] \ No newline at end of file diff --git a/tests/test_cloudevent/test_serialize/test_base64protobufserializer.py b/tests/test_cloudevent/test_serialize/test_base64protobufserializer.py new file mode 100644 index 0000000..3148ab9 --- /dev/null +++ b/tests/test_cloudevent/test_serialize/test_base64protobufserializer.py @@ -0,0 +1,77 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import unittest + +from uprotocol.cloudevent.datamodel.ucloudeventattributes import UCloudEventAttributesBuilder +from uprotocol.cloudevent.factory.cloudeventfactory import CloudEventFactory +from uprotocol.cloudevent.serialize.base64protobufserializer import Base64ProtobufSerializer +from uprotocol.cloudevent.serialize.cloudeventserializers import CloudEventSerializers + + +class TestBase64ProtobufSerializer(unittest.TestCase): + + def test_deserialize_bytes_to_string(self): + ce = CloudEventFactory.build_base_cloud_event("hello", "http://localhost", bytearray(), "", + UCloudEventAttributesBuilder().build(), "example.vertx") + ce.__delitem__("time") + bytes_data = CloudEventSerializers.PROTOBUF.serializer().serialize(ce) + payload = Base64ProtobufSerializer().deserialize(bytes_data) + self.assertEqual( + "CgVoZWxsbxIQaHR0cDovL2xvY2FsaG9zdBoDMS4wIg1leGFtcGxlLnZlcnR4", + payload) + + def test_deserialize_bytes_to_string_when_bytes_is_null(self): + payload = Base64ProtobufSerializer().deserialize(None) + self.assertEqual("", payload) + + def test_deserialize_bytes_to_string_when_bytes_is_empty(self): + payload = Base64ProtobufSerializer().deserialize(bytearray()) + self.assertEqual("", payload) + + def test_serialize_string_into_bytes(self): + json_str = "eyJzcGVjdmVyc2lvbiI6ICIxLjAiLCAiaWQiOiAiaGVsbG8iLCAic291cmNlIjogImh0dHA6Ly9sb2NhbGhvc3QiLCAidHlwZSI6ICJleGFtcGxlLnZlcnR4IiwgImRhdGFfYmFzZTY0IjogIiJ9" + bytes_json = Base64ProtobufSerializer().serialize(json_str) + + ce = CloudEventFactory.build_base_cloud_event("hello", "http://localhost", bytearray(), "", + UCloudEventAttributesBuilder().build(), "example.vertx") + ce.__delitem__("time") + + bytes_data = CloudEventSerializers.JSON.serializer().serialize(ce) + self.assertEqual(bytes_json, bytes_data) + + def test_serialize_string_into_bytes_when_string_is_null(self): + bytes_data = Base64ProtobufSerializer().serialize(None) + self.assertEqual(bytearray(), bytes_data) + + def test_serialize_string_into_bytes_when_string_is_empty(self): + bytes_data = Base64ProtobufSerializer().serialize('') + self.assertEqual(bytearray(), bytes_data) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_cloudevent/test_serialize/test_cloudeventtojsonserializer.py b/tests/test_cloudevent/test_serialize/test_cloudeventtojsonserializer.py new file mode 100644 index 0000000..a4a58e2 --- /dev/null +++ b/tests/test_cloudevent/test_serialize/test_cloudeventtojsonserializer.py @@ -0,0 +1,129 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import unittest + +from google.protobuf import any_pb2 + +from uprotocol.cloudevent.datamodel.ucloudeventattributes import UCloudEventAttributesBuilder +from uprotocol.cloudevent.factory.cloudeventfactory import CloudEventFactory +from uprotocol.cloudevent.factory.ucloudevent import UCloudEvent +from uprotocol.cloudevent.serialize.cloudeventserializers import CloudEventSerializers +from uprotocol.cloudevent.serialize.cloudeventtojsonserializer import CloudEventToJsonSerializer +from uprotocol.proto.cloudevents_pb2 import CloudEvent +from uprotocol.proto.uattributes_pb2 import UPriority, UMessageType + +protoContentType = CloudEventFactory.PROTOBUF_CONTENT_TYPE +serializer = CloudEventSerializers.JSON.serializer() + + +def build_proto_payload_for_test(): + ce_proto = CloudEvent(spec_version="1.0", source="https://example.com", id="hello", type="example.demo", + proto_data=any_pb2.Any(), + attributes={"ttl": CloudEvent.CloudEventAttributeValue(ce_string="3")}) + + any_obj = any_pb2.Any() + any_obj.Pack(ce_proto) + return any_obj + + +class TestCloudEventToJsonSerializer(unittest.TestCase): + + def test_serialize_cloud_event_to_json(self): + proto_payload = build_proto_payload_for_test() + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).build() + + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("hello", "/body.access/1/door.front_left", + proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + cloud_event.__setitem__('datacontenttype', protoContentType) + cloud_event.__setitem__('dataschema', proto_payload.type_url) + cloud_event.__delitem__("time") + bytes_data = serializer.serialize(cloud_event) + json_str = bytes_data.decode('utf-8') + expected = ('{"specversion": "1.0", "id": "hello", "source": "/body.access/1/door.front_left", ' + '"type": "pub.v1", "datacontenttype": "application/x-protobuf", "dataschema": ' + '"type.googleapis.com/io.cloudevents.v1.CloudEvent", "data_base64": ' + '"CjB0eXBlLmdvb2dsZWFwaXMuY29tL2lvLmNsb3VkZXZlbnRzLnYxLkNsb3VkRXZlbnQSPQoFaGVsbG8SE2h0dHBzOi8vZXhhbXBsZS5jb20aAzEuMCIMZXhhbXBsZS5kZW1vKgoKA3R0bBIDGgEzQgA=", "ttl": 3, "priority": "UPRIORITY_CS1"}') + self.assertEqual(expected, json_str) + + def test_serialize_and_deserialize_cloud_event_to_json(self): + proto_payload = build_proto_payload_for_test() + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).build() + + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("hello", "/body.access/1/door.front_left", + proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + cloud_event.__setitem__('datacontenttype', protoContentType) + cloud_event.__setitem__('dataschema', proto_payload.type_url) + cloud_event.__delitem__("time") + serialized_data = serializer.serialize(cloud_event) + deserialized_data = serializer.deserialize(serialized_data) + deserialized_data.__delitem__("time") + + self.assertEqual(cloud_event, deserialized_data) + + def test_double_serialization_protobuf_when_creating_cloud_event_with_factory_methods(self): + proto_payload = build_proto_payload_for_test() + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).build() + + # build the cloud event + cloud_event1 = CloudEventFactory.build_base_cloud_event("hello", "/body.access/1/door.front_left", + proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + cloud_event1.__setitem__('datacontenttype', protoContentType) + cloud_event1.__setitem__('dataschema', proto_payload.type_url) + cloud_event1.__delitem__("time") + serialized_data1 = serializer.serialize(cloud_event1) + cloud_event2 = serializer.deserialize(serialized_data1) + cloud_event2.__delitem__("time") + self.assertEqual(cloud_event2, cloud_event1) + serialized_data2 = serializer.serialize(cloud_event2) + self.assertEqual(serialized_data1, serialized_data2) + cloud_event3 = serializer.deserialize(serialized_data2) + cloud_event3.__delitem__("time") + + payload1 = UCloudEvent.unpack(cloud_event3, CloudEvent) + self.assertEqual(cloud_event2, cloud_event3) + payload2 = CloudEvent() + payload2.ParseFromString(proto_payload.value) + self.assertEqual(payload1, payload2) + self.assertEqual(cloud_event1, cloud_event3) diff --git a/tests/test_cloudevent/test_serialize/test_cloudeventtoprotobufserializer.py b/tests/test_cloudevent/test_serialize/test_cloudeventtoprotobufserializer.py new file mode 100644 index 0000000..638cb92 --- /dev/null +++ b/tests/test_cloudevent/test_serialize/test_cloudeventtoprotobufserializer.py @@ -0,0 +1,157 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + +import json +import os +import unittest + +from cloudevents.http import CloudEvent as ApacheCloudEvent +from google.protobuf import any_pb2 + +from uprotocol.cloudevent.datamodel.ucloudeventattributes import UCloudEventAttributesBuilder +from uprotocol.cloudevent.factory.cloudeventfactory import CloudEventFactory +from uprotocol.cloudevent.factory.ucloudevent import UCloudEvent +from uprotocol.cloudevent.serialize.base64protobufserializer import Base64ProtobufSerializer +from uprotocol.cloudevent.serialize.cloudeventserializers import CloudEventSerializers +from uprotocol.cloudevent.serialize.cloudeventtoprotobufserializer import CloudEventToProtobufSerializer +from uprotocol.proto.cloudevents_pb2 import CloudEvent +from uprotocol.proto.uattributes_pb2 import UPriority, UMessageType +from uprotocol.proto.uri_pb2 import UUri, UEntity, UResource +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer + +serializer = CloudEventSerializers.PROTOBUF.serializer() + + +def build_uuri_for_test(): + uri = UUri(entity=UEntity(name="body.access"), + resource=UResource(name="door", instance="front_left", message="Door")) + return LongUriSerializer().serialize(uri) + + +def build_proto_payload_for_test(): + ce_proto = CloudEvent(spec_version="1.0", source="https://example.com", id="hello", type="example.demo", + proto_data=any_pb2.Any(), + attributes={"ttl": CloudEvent.CloudEventAttributeValue(ce_string="3")}) + + any_obj = any_pb2.Any() + any_obj.Pack(ce_proto) + return any_obj + + +def get_json_object(): + current_directory = os.getcwd() + json_file_path = os.path.join(current_directory, "tests","test_cloudevent", + "test_serialize","cloudevent_to_protobuf.json") + with open(json_file_path, 'r') as json_file: + json_data = json.load(json_file) + + return json_data + + +class TestCloudEventToProtobufSerializer(unittest.TestCase): + + def test_all_cloud_events_from_json(self): + cloudevents = get_json_object() + for ce_json in cloudevents: + bytes_ce = Base64ProtobufSerializer().serialize(ce_json['serialized_ce']) + cloudevent = CloudEventToProtobufSerializer().deserialize(bytes_ce) + self.assertEqual(UCloudEvent.get_id(cloudevent), ce_json['id']) + self.assertEqual(UCloudEvent.get_specversion(cloudevent), ce_json['specversion']) + if 'source' in ce_json: + self.assertEqual(UCloudEvent.get_source(cloudevent), ce_json['source']) + if 'sink' in ce_json: + self.assertEqual(UCloudEvent.get_sink(cloudevent), ce_json['sink']) + if 'type' in ce_json: + self.assertEqual(UCloudEvent.get_type(cloudevent), ce_json['type']) + if 'priority' in ce_json: + self.assertEqual(UCloudEvent.get_priority(cloudevent), ce_json['priority']) + if 'ttl' in ce_json: + self.assertEqual(UCloudEvent.get_ttl(cloudevent), ce_json['ttl']) + if 'hash' in ce_json: + self.assertEqual(UCloudEvent.get_hash(cloudevent), ce_json['hash']) + if 'token' in ce_json: + self.assertEqual(UCloudEvent.get_token(cloudevent), ce_json['token']) + if 'dataschema' in ce_json: + self.assertEqual(UCloudEvent.get_data_schema(cloudevent), ce_json['dataschema']) + if 'datacontenttype' in ce_json: + self.assertEqual(UCloudEvent.get_data_content_type(cloudevent), ce_json['datacontenttype']) + if 'commstatus' in ce_json: + self.assertEqual(UCloudEvent.get_communication_status(cloudevent), ce_json['commstatus']) + + def test_serialize_and_deserialize_cloud_event_to_protobuf(self): + source = build_uuri_for_test() + proto_payload = build_proto_payload_for_test() + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS0).with_ttl(3).build() + + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("hello", "/body.access/1/door.front_left", + proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + + cloud_event.__delitem__("time") + serialized_data = serializer.serialize(cloud_event) + + deserialized_data = serializer.deserialize(serialized_data) + deserialized_data.__delitem__("time") + self.assertEqual(cloud_event, deserialized_data) + + def test_serialize_two_different_cloud_event_are_not_the_same(self): + proto_payload = build_proto_payload_for_test() + json_attributes1 = {"id": "hello", "source": "/body.access/1/door.front_left", "type": "pub.v1"} + cloud_event1 = ApacheCloudEvent(json_attributes1, proto_payload.SerializeToString()) + cloud_event1.__setitem__('datacontenttype', 'application/protobuf') + cloud_event1.__setitem__('dataschema', proto_payload.type_url) + cloud_event1.__delitem__("time") + + json_attributes2 = {"source": "/body.access/1/door.front_left", "type": "file.v1"} + cloud_event2 = ApacheCloudEvent(json_attributes2, proto_payload.SerializeToString()) + cloud_event2.__delitem__("time") + serialized1 = serializer.serialize(cloud_event1) + serialized2 = serializer.serialize(cloud_event2) + self.assertNotEqual(serialized1, serialized2) + + def test_double_serialization_protobuf_when_creating_cloud_event_with_factory_methods(self): + proto_payload = build_proto_payload_for_test() + source = build_uuri_for_test() + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).with_token("someOAuthToken").build() + + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("testme", source, proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + cloud_event.__delitem__("time") + + serialized_data = serializer.serialize(cloud_event) + deserialized_data = serializer.deserialize(serialized_data) + deserialized_data.__delitem__("time") + self.assertEqual(cloud_event, deserialized_data) + diff --git a/tests/test_cloudevent/test_validator/__init__.py b/tests/test_cloudevent/test_validator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_cloudevent/test_validator/test_cloudeventvalidator.py b/tests/test_cloudevent/test_validator/test_cloudeventvalidator.py new file mode 100644 index 0000000..228cf2f --- /dev/null +++ b/tests/test_cloudevent/test_validator/test_cloudeventvalidator.py @@ -0,0 +1,470 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + + +import unittest + +from google.protobuf import any_pb2 + +from uprotocol.cloudevent.datamodel.ucloudeventattributes import UCloudEventAttributesBuilder +from uprotocol.cloudevent.factory.cloudeventfactory import CloudEventFactory +from uprotocol.cloudevent.factory.ucloudevent import UCloudEvent +from uprotocol.cloudevent.validate.cloudeventvalidator import CloudEventValidator, Validators +from uprotocol.proto.cloudevents_pb2 import CloudEvent +from uprotocol.proto.uattributes_pb2 import UPriority, UMessageType +from uprotocol.proto.uri_pb2 import UUri, UEntity, UResource +from uprotocol.proto.ustatus_pb2 import UCode +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer +from uprotocol.uuid.factory.uuidfactory import Factories +from uprotocol.uuid.serializer.longuuidserializer import LongUuidSerializer +from uprotocol.validation.validationresult import ValidationResult + + +def build_base_cloud_event_for_test(): + # uri + source = build_long_uri_for_test() + + # fake payload + proto_payload = build_proto_payload_for_test() + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( + UPriority.UPRIORITY_CS1).with_ttl(3).with_token("someOAuthToken").build() + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event("testme", source, proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + return cloud_event + + pass + + +def build_proto_payload_for_test(): + ce_proto = CloudEvent(spec_version="1.0", source="https://example.com", id="hello", type="example.demo", + proto_data=any_pb2.Any()) + + any_obj = any_pb2.Any() + any_obj.Pack(ce_proto) + return any_obj + + +def build_uuri_for_test(): + return UUri(entity=UEntity(name="body.access"), + resource=UResource(name="door", instance="front_left", message="Door")) + + +def build_long_uri_for_test(): + return LongUriSerializer().serialize(build_uuri_for_test()) + + +class TestCloudEventValidator(unittest.TestCase): + + def test_get_a_publish_cloud_event_validator(self): + cloud_event = build_base_cloud_event_for_test() + validator = CloudEventValidator.get_validator(cloud_event) + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(status, ValidationResult.STATUS_SUCCESS) + self.assertEqual("CloudEventValidator.Publish", str(validator)) + + def test_get_a_notification_cloud_event_validator(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("sink", "//bo.cloud/petapp") + validator = Validators.NOTIFICATION.validator() + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(status, ValidationResult.STATUS_SUCCESS) + self.assertEqual("CloudEventValidator.Notification", str(validator)) + + def test_publish_cloud_event_type(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "res.v1") + validator = Validators.PUBLISH.validator() + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid CloudEvent type [res.v1]. CloudEvent of type Publish must have a type of 'pub.v1'", + status.message) + + def test_notification_cloud_event_type(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "res.v1") + validator = Validators.NOTIFICATION.validator() + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid CloudEvent type [res.v1]. CloudEvent of type Publish must have a type of 'pub.v1'", + status.message) + + def test_get_a_request_cloud_event_validator(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "req.v1") + validator = CloudEventValidator.get_validator(cloud_event) + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(status, ValidationResult.STATUS_SUCCESS) + self.assertEqual("CloudEventValidator.Request", str(validator)) + + def test_request_cloud_event_type(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "pub.v1") + validator = Validators.REQUEST.validator() + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid CloudEvent type [pub.v1]. CloudEvent of type Request must have a type of 'req.v1'", + status.message) + + def test_get_a_response_cloud_event_validator(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "res.v1") + validator = CloudEventValidator.get_validator(cloud_event) + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(status, ValidationResult.STATUS_SUCCESS) + self.assertEqual("CloudEventValidator.Response", str(validator)) + + def test_response_cloud_event_type(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "pub.v1") + validator = Validators.RESPONSE.validator() + status = validator.validate_type(cloud_event).to_status() + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid CloudEvent type [pub.v1]. CloudEvent of type Response must have a type of 'res.v1'", + status.message) + + def test_get_a_publish_cloud_event_validator_when_cloud_event_type_is_unknown(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "lala.v1") + validator = CloudEventValidator.get_validator(cloud_event) + status = validator.validate_type(cloud_event).to_status() + self.assertEqual("CloudEventValidator.Publish", str(validator)) + + def test_validate_cloud_event_version_when_valid(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("id", str_uuid) + status = CloudEventValidator.validate_version(cloud_event).to_status() + self.assertEqual(status, ValidationResult.STATUS_SUCCESS) + + def test_validate_cloud_event_version_when_not_valid(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("specversion", "0.3") + cloud_event.__setitem__("id", str_uuid) + status = CloudEventValidator.validate_version(cloud_event).to_status() + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid CloudEvent version [0.3]. CloudEvent version must be 1.0.", status.message) + + def test_validate_cloud_event_id_when_valid(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + status = CloudEventValidator.validate_version(cloud_event).to_status() + self.assertEqual(status, ValidationResult.STATUS_SUCCESS) + + def test_validate_cloud_event_id_when_not_uuidv8_type_id(self): + str_uuid = "1dd9200c-d41b-4658-8102-3101f0b91378" + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + status = CloudEventValidator.validate_id(cloud_event).to_status() + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid CloudEvent Id [" + str_uuid + "]. CloudEvent Id must be of type UUIDv8.", + status.message) + + def test_validate_cloud_event_id_when_not_valid(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", "testme") + cloud_event.__setitem__("type", "pub.v1") + status = CloudEventValidator.validate_id(cloud_event).to_status() + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid CloudEvent Id [testme]. CloudEvent Id must be of type UUIDv8.", status.message) + + def test_publish_type_cloudevent_is_valid_when_everything_is_valid_local(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "/body.access/1/door.front_left#Door") + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertEqual(ValidationResult.success(), result) + + def test_publish_type_cloudevent_is_valid_when_everything_is_valid_remote(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "//VCU.myvin/body.access/1/door.front_left#Door") + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertEqual(ValidationResult.success(), result) + + def test_publish_type_cloudevent_is_valid_when_everything_is_valid_remote_with_a_sink(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "//VCU.myvin/body.access/1/door.front_left#Door") + cloud_event.__setitem__("sink", "//bo.cloud/petapp") + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertEqual(ValidationResult.success(), result) + + def test_publish_type_cloudevent_is_not_valid_when_remote_with_invalid_sink(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "//VCU.myvin/body.access/1/door.front_left#Door") + cloud_event.__setitem__("sink", "//bo.cloud") + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertEqual("Invalid CloudEvent sink [//bo.cloud]. Uri is missing uSoftware Entity name.", + result.get_message()) + + def test_publish_type_cloudevent_is_not_valid_when_source_is_empty(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("source", "/") + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertEqual("Invalid Publish type CloudEvent source [/]. Uri is empty.", result.get_message()) + + def test_publish_type_cloudevent_is_not_valid_when_source_is_missing_authority(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", "testme") + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "/body.access") + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid CloudEvent Id [testme]. CloudEvent Id must be of type UUIDv8.," + "Invalid Publish type " + + "CloudEvent source [/body.access]. UriPart is missing uResource name.", + result.get_message()) + + def test_publish_type_cloudevent_is_not_valid_when_source_is_missing_message_info(self): + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", "testme") + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "/body.access/1/door.front_left") + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid CloudEvent Id [testme]. CloudEvent Id must be of type UUIDv8.," + "Invalid Publish type " + + "CloudEvent source [/body.access/1/door.front_left]. UriPart is missing Message information.", + result.get_message()) + + def test_notification_type_cloudevent_is_valid_when_everything_is_valid(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "/body.access/1/door.front_left#Door") + cloud_event.__setitem__("sink", "//bo.cloud/petapp") + validator = Validators.NOTIFICATION.validator() + result = validator.validate(cloud_event) + self.assertEqual(ValidationResult.success(), result) + + def test_notification_type_cloudevent_is_not_valid_missing_sink(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("source", "/body.access/1/door.front_left#Door") + validator = Validators.NOTIFICATION.validator() + result = validator.validate(cloud_event) + self.assertEqual("Invalid CloudEvent sink. Notification CloudEvent sink must be an uri.", + result.get_message()) + + def test_notification_type_cloudevent_is_not_valid_invalid_sink(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "pub.v1") + cloud_event.__setitem__("sink", "//bo.cloud") + cloud_event.__setitem__("source", "/body.access/1/door.front_left#Door") + validator = Validators.NOTIFICATION.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid Notification type CloudEvent sink [//bo.cloud]. Uri is missing uSoftware Entity name.", + result.get_message()) + + def test_request_type_cloudevent_is_valid_when_everything_is_valid(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "req.v1") + cloud_event.__setitem__("sink", "//VCU.myvin/body.access/1/rpc.UpdateDoor") + cloud_event.__setitem__("source", "//bo.cloud/petapp//rpc.response") + validator = Validators.REQUEST.validator() + result = validator.validate(cloud_event) + self.assertEqual(ValidationResult.success(), result) + + def test_request_type_cloudevent_is_not_valid_invalid_source(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "req.v1") + cloud_event.__setitem__("sink", "//VCU.myvin/body.access/1/rpc.UpdateDoor") + cloud_event.__setitem__("source", "//bo.cloud/petapp//dog") + validator = Validators.REQUEST.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid RPC Request CloudEvent source [//bo.cloud/petapp//dog]. " + "Invalid RPC uri application " + + "response topic. UriPart is missing rpc.response.", + result.get_message()) + + def test_request_type_cloudevent_is_not_valid_missing_sink(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "req.v1") + cloud_event.__setitem__("source", "//bo.cloud/petapp//rpc.response") + validator = Validators.REQUEST.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid RPC Request CloudEvent sink. Request CloudEvent sink must be uri for the method to be called.", + result.get_message()) + + def test_request_type_cloudevent_is_not_valid_invalid_sink_not_rpc_command(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "req.v1") + cloud_event.__setitem__("source", "//bo.cloud/petapp//rpc.response") + cloud_event.__setitem__("sink", "//VCU.myvin/body.access/1/UpdateDoor") + validator = Validators.REQUEST.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid RPC Request CloudEvent sink [//VCU.myvin/body.access/1/UpdateDoor]. " + "Invalid RPC method " + + "uri. UriPart should be the method to be called, or method from response.", + result.get_message()) + + def test_response_type_cloudevent_is_valid_when_everything_is_valid(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "res.v1") + cloud_event.__setitem__("sink", "//bo.cloud/petapp//rpc.response") + cloud_event.__setitem__("source", "//VCU.myvin/body.access/1/rpc.UpdateDoor") + validator = Validators.RESPONSE.validator() + result = validator.validate(cloud_event) + self.assertEqual(ValidationResult.success(), result) + + def test_response_type_cloudevent_is_not_valid_invalid_source(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "res.v1") + cloud_event.__setitem__("sink", "//bo.cloud/petapp//rpc.response") + cloud_event.__setitem__("source", "//VCU.myvin/body.access/1/UpdateDoor") + validator = Validators.RESPONSE.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid RPC Response CloudEvent source [//VCU.myvin/body.access/1/UpdateDoor]. " + "Invalid RPC " + + "method uri. UriPart should be the method to be called, or method from response.", + result.get_message()) + + def test_response_type_cloudevent_is_not_valid_missing_sink_and_invalid_source(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "res.v1") + cloud_event.__setitem__("source", "//VCU.myvin/body.access/1/UpdateDoor") + validator = Validators.RESPONSE.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid RPC Response CloudEvent source [//VCU.myvin/body.access/1/UpdateDoor]. " + "Invalid RPC " + + "method uri. UriPart should be the method to be called, or method from response.," + "Invalid" + " CloudEvent sink. Response CloudEvent sink must be uri the destination of the response.", + result.get_message()) + + def test_response_type_cloudevent_is_not_valid_invalid_sink(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "res.v1") + cloud_event.__setitem__("sink", "//bo.cloud") + cloud_event.__setitem__("source", "//VCU.myvin") + validator = Validators.RESPONSE.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid RPC Response CloudEvent source [//VCU.myvin]. Invalid RPC method uri. Uri is missing " + + "uSoftware Entity name.,Invalid RPC Response CloudEvent sink [//bo.cloud]. Invalid RPC uri " + + "application response topic. Uri is missing uSoftware Entity name.", + result.get_message()) + + def test_response_type_cloudevent_is_not_valid_invalid_source_not_rpc_command(self): + uuid = Factories.UPROTOCOL.create() + str_uuid = LongUuidSerializer.instance().serialize(uuid) + cloud_event = build_base_cloud_event_for_test() + cloud_event.__setitem__("id", str_uuid) + cloud_event.__setitem__("type", "res.v1") + cloud_event.__setitem__("source", "//bo.cloud/petapp/1/dog") + cloud_event.__setitem__("sink", "//VCU.myvin/body.access/1/UpdateDoor") + validator = Validators.RESPONSE.validator() + result = validator.validate(cloud_event) + self.assertEqual( + "Invalid RPC Response CloudEvent source [//bo.cloud/petapp/1/dog]. Invalid RPC method uri. UriPart " + + "should be the method to be called, or method from response.," + "Invalid RPC Response " + "CloudEvent " + "sink [" + "//VCU.myvin/body.access/1/UpdateDoor]. " + "Invalid RPC uri application " + "response topic. UriPart is missing rpc.response.", + result.get_message()) + + def test_create_a_v6_cloudevent_and_validate_it_against_sdk(self): + source = build_long_uri_for_test() + uuid = Factories.UUIDV6.create() + id = LongUuidSerializer.instance().serialize(uuid) + proto_payload=build_proto_payload_for_test() + # additional attributes + u_cloud_event_attributes = UCloudEventAttributesBuilder().with_priority( + UPriority.UPRIORITY_CS0).with_ttl(1000).build() + # build the cloud event + cloud_event = CloudEventFactory.build_base_cloud_event(id, source, proto_payload.SerializeToString(), + proto_payload.type_url, u_cloud_event_attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + validator = Validators.PUBLISH.validator() + result = validator.validate(cloud_event) + self.assertTrue(result.is_success()) + self.assertFalse(UCloudEvent.is_expired(cloud_event)) diff --git a/tests/test_cloudevent/test_validator/test_validationresult.py b/tests/test_cloudevent/test_validator/test_validationresult.py new file mode 100644 index 0000000..61d9dcf --- /dev/null +++ b/tests/test_cloudevent/test_validator/test_validationresult.py @@ -0,0 +1,67 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import unittest + +from uprotocol.validation.validationresult import ValidationResult +from uprotocol.proto.ustatus_pb2 import UStatus, UCode + + +class TestValidationResultTest(unittest.TestCase): + + def test_success_validation_result_to_string(self): + success = ValidationResult.success() + self.assertEqual("ValidationResult.Success()", str(success)) + + def test_failure_validation_result_to_string(self): + failure = ValidationResult.failure("boom") + self.assertEqual("ValidationResult.Failure(message='boom')", str(failure)) + + def test_success_validation_result_is_success(self): + success = ValidationResult.success() + self.assertTrue(success.is_success()) + + def test_failure_validation_result_is_success(self): + failure = ValidationResult.failure("boom") + self.assertFalse(failure.is_success()) + + def test_success_validation_result_get_message(self): + success = ValidationResult.success() + self.assertTrue(success.get_message() == '') + + def test_failure_validation_result_get_message(self): + failure = ValidationResult.failure("boom") + self.assertEqual("boom", failure.get_message()) + + def test_success_validation_result_to_status(self): + success = ValidationResult.success() + self.assertEqual(ValidationResult.STATUS_SUCCESS, success.to_status()) + + def test_failure_validation_result_to_status(self): + failure = ValidationResult.failure("boom") + status = UStatus(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(status, failure.to_status()) diff --git a/tests/test_rpc/__init__.py b/tests/test_rpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_rpc/test_calloptions.py b/tests/test_rpc/test_calloptions.py new file mode 100644 index 0000000..18fa71c --- /dev/null +++ b/tests/test_rpc/test_calloptions.py @@ -0,0 +1,81 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +import unittest + +from uprotocol.rpc.calloptions import CallOptions, CallOptionsBuilder + + +class TestCallOptions(unittest.TestCase): + + def test_hash_code_equals(self): + call_options = CallOptionsBuilder().build() + self.assertEqual(hash(call_options), hash(call_options)) + + def test_to_string(self): + call_options = CallOptionsBuilder().with_timeout(30).with_token("someToken").build() + expected = "CallOptions{mTimeout=30, mToken='someToken'}" + self.assertEqual(expected, str(call_options)) + + def test_creating_call_options_default(self): + call_options = CallOptionsBuilder.DEFAULT + self.assertEqual(CallOptions.TIMEOUT_DEFAULT, call_options.get_timeout()) + self.assertTrue(call_options.get_token() is None or call_options.get_token() == "") + + def test_creating_call_options_with_a_token(self): + call_options = CallOptionsBuilder().with_token("someToken").build() + self.assertEqual(CallOptions.TIMEOUT_DEFAULT, call_options.get_timeout()) + self.assertTrue(call_options.get_token() == "someToken") + + def test_creating_call_options_with_a_null_token(self): + call_options = CallOptionsBuilder().with_token(None).build() + self.assertEqual(CallOptions.TIMEOUT_DEFAULT, call_options.get_timeout()) + self.assertTrue(call_options.get_token() is None or call_options.get_token() == "") + + def test_creating_call_options_with_an_empty_string_token(self): + call_options = CallOptionsBuilder().with_token("").build() + self.assertEqual(CallOptions.TIMEOUT_DEFAULT, call_options.get_timeout()) + self.assertTrue(call_options.get_token() is None or call_options.get_token() == "") + + def test_creating_call_options_with_a_token_with_only_spaces(self): + call_options = CallOptionsBuilder().with_token(" ").build() + self.assertEqual(CallOptions.TIMEOUT_DEFAULT, call_options.get_timeout()) + self.assertTrue(call_options.get_token() is None or call_options.get_token().isspace()) + + def test_creating_call_options_with_a_timeout(self): + call_options = CallOptionsBuilder().with_timeout(30).build() + self.assertEqual(30, call_options.get_timeout()) + self.assertTrue(call_options.get_token() is None or call_options.get_token() == "") + + def test_creating_call_options_with_a_negative_timeout(self): + call_options = CallOptionsBuilder().with_timeout(-3).build() + self.assertEqual(CallOptions.TIMEOUT_DEFAULT, call_options.get_timeout()) + self.assertTrue(call_options.get_token() is None or call_options.get_token() == "") + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_rpc/test_rpc.py b/tests/test_rpc/test_rpc.py new file mode 100644 index 0000000..6c1001e --- /dev/null +++ b/tests/test_rpc/test_rpc.py @@ -0,0 +1,241 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import unittest +from concurrent.futures import Future +from google.protobuf.any_pb2 import Any +from google.protobuf.wrappers_pb2 import Int32Value +from uprotocol.proto.cloudevents_pb2 import CloudEvent +from uprotocol.proto.uattributes_pb2 import UPriority +from uprotocol.proto.upayload_pb2 import UPayload, UPayloadFormat +from uprotocol.proto.uri_pb2 import UUri, UEntity +from uprotocol.proto.ustatus_pb2 import UStatus, UCode +from uprotocol.rpc.rpcclient import RpcClient +from uprotocol.rpc.rpcmapper import RpcMapper +from uprotocol.rpc.rpcresult import RpcResult +from uprotocol.transport.builder.uattributesbuilder import UAttributesBuilder +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer + + +def build_cloud_event(): + return CloudEvent(spec_version="1.0", source="https://example.com", id="HARTLEY IS THE BEST") + + +def build_upayload(): + any_obj = Any() + any_obj.Pack(build_cloud_event()) + return UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, value=any_obj.SerializeToString()) + + +def build_topic(): + return LongUriSerializer().deserialize("//vcu.vin/hartley/1/rpc.Raise") + + +def build_uattributes(): + return UAttributesBuilder.request(UPriority.UPRIORITY_CS4, UUri(entity=UEntity(name="hartley")), 1000).build() + + +class ReturnsNumber3(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + any_obj = Any() + any_obj.Pack(Int32Value(value=3)) + data = UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, value=any_obj.SerializeToString()) + future.set_result(data) + return future + + +class HappyPath(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + data = build_upayload() + future.set_result(data) + return future + + +class WithUStatusCodeInsteadOfHappyPath(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + status = UStatus(code=UCode.INVALID_ARGUMENT, message="boom") + any_value = Any() + any_value.Pack(status) + data = UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, value=any_value.SerializeToString()) + future.set_result(data) + return future + + +class WithUStatusCodeHappyPath(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + status = UStatus(code=UCode.OK, message="all good") + any_value = Any() + any_value.Pack(status) + data = UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, value=any_value.SerializeToString()) + future.set_result(data) + return future + + +class ThatBarfsCrapyPayload(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + response = UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_RAW, value=bytes([0])) + future.set_result(response) + return future + + +class ThatCompletesWithAnException(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + future.set_exception(RuntimeError("Boom")) + return future + + +class ThatReturnsTheWrongProto(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + any_value = Any() + any_value.Pack(Int32Value(value=42)) + data = UPayload(format=UPayloadFormat.UPAYLOAD_FORMAT_PROTOBUF, value=any_value.SerializeToString()) + future.set_result(data) + return future + + +class WithNullInPayload(RpcClient): + def invoke_method(self, topic, payload, attributes): + future = Future() + future.set_result(None) + return future + + +def rpc_response(invoke_method_response: Future): + payload, exception = invoke_method_response.result() + + any_value = Any() + try: + any_value.ParseFromString(payload.value) + except Exception as e: + raise RuntimeError(str(e)) from e + + # invoke method had some unexpected problem. + if exception is not None: + raise RuntimeError(str(exception)) from exception + + # test to see if we have the expected type + if any_value.Is(CloudEvent.DESCRIPTOR): + try: + cloud_event = CloudEvent() + any_value.Unpack(cloud_event) + return cloud_event + except Exception as e: + raise RuntimeError(str(e)) from e + + # this will be called only if the expected return type is not status, but status was returned to + # indicate a problem. + if any_value.Is(UStatus.DESCRIPTOR): + try: + status = UStatus() + any_value.Unpack(status) + raise RuntimeError(f"Error returned, status code: [{status.code}], message: [{status.message}]") + except Exception as e: + raise RuntimeError(f"{str(e)} [com.google.grpc.UStatus]") from e + + raise RuntimeError(f"Unknown payload type [{any_value.type_url}]") + + +class TestRpc(unittest.TestCase): + + def test_compose_happy_path(self): + rpc_response = RpcMapper.map_response_to_result( + ReturnsNumber3().invoke_method(build_topic(), build_upayload(), build_uattributes()), Int32Value) + mapped = rpc_response.map(lambda x: x.value + 5) + self.assertTrue(rpc_response.isSuccess()) + self.assertEqual(8, mapped.successValue()) + + def test_compose_that_returns_status(self): + rpc_response = RpcMapper.map_response_to_result( + WithUStatusCodeInsteadOfHappyPath().invoke_method(build_topic(), build_upayload(), build_uattributes()), + Int32Value) + mapped = rpc_response.map(lambda x: x.value + 5) + self.assertTrue(rpc_response.isFailure()) + self.assertEqual(UCode.INVALID_ARGUMENT, mapped.failureValue().code) + self.assertEqual("boom", mapped.failureValue().message) + + def test_compose_with_failure(self): + rpc_response = RpcMapper.map_response_to_result( + ThatCompletesWithAnException().invoke_method(build_topic(), build_upayload(), build_uattributes()), + Int32Value) + mapped = rpc_response.map(lambda x: x.value + 5) + self.assertTrue(rpc_response.isFailure()) + status = UStatus(code=UCode.UNKNOWN, message="Boom") + self.assertEqual(status, mapped.failureValue()) + + def test_success_invoke_method_happy_flow_using_mapResponseToRpcResponse(self): + rpc_response = RpcMapper.map_response_to_result( + HappyPath().invoke_method(build_topic(), build_upayload(), build_uattributes()), + CloudEvent) + self.assertTrue(rpc_response.isSuccess()) + self.assertEqual(build_cloud_event(), rpc_response.successValue()) + + def test_fail_invoke_method_when_invoke_method_returns_a_status_using_mapResponseToRpcResponse(self): + rpc_response = RpcMapper.map_response_to_result( + WithUStatusCodeInsteadOfHappyPath().invoke_method(build_topic(), build_upayload(), build_uattributes()), + CloudEvent) + self.assertTrue(rpc_response.isFailure()) + self.assertEqual(UCode.INVALID_ARGUMENT, rpc_response.failureValue().code) + self.assertEqual("boom", rpc_response.failureValue().message) + + def test_fail_invoke_method_when_invoke_method_threw_an_exception_using_mapResponseToRpcResponse(self): + rpc_response = RpcMapper.map_response_to_result( + ThatCompletesWithAnException().invoke_method(build_topic(), build_upayload(), build_uattributes()), + CloudEvent) + self.assertTrue(rpc_response.isFailure()) + self.assertEqual(UCode.UNKNOWN, rpc_response.failureValue().code) + self.assertEqual("Boom", rpc_response.failureValue().message) + + def test_fail_invoke_method_when_invoke_method_returns_a_bad_proto_using_mapResponseToRpcResponse(self): + rpc_response = RpcMapper.map_response_to_result( + ThatReturnsTheWrongProto().invoke_method(build_topic(), build_upayload(), build_uattributes()), + CloudEvent) + self.assertTrue(rpc_response.isFailure()) + self.assertEqual(UCode.UNKNOWN, rpc_response.failureValue().code) + self.assertEqual("Unknown payload type [type.googleapis.com/google.protobuf.Int32Value]. Expected [io.cloudevents.v1.CloudEvent]", rpc_response.failureValue().message) + + def test_success_invoke_method_happy_flow_using_mapResponse(self): + rpc_response = RpcMapper.map_response( + HappyPath().invoke_method(build_topic(), build_upayload(), build_uattributes()), + CloudEvent) + self.assertEqual(build_cloud_event(), rpc_response.result()) + + def test_fail_invoke_method_when_invoke_method_returns_a_status_using_mapResponse(self): + rpc_response = RpcMapper.map_response( + WithUStatusCodeInsteadOfHappyPath().invoke_method(build_topic(), build_upayload(), build_uattributes()), + CloudEvent) + exception=RuntimeError("Unknown payload type [type.googleapis.com/uprotocol.v1.UStatus]. Expected [CloudEvent]") + self.assertEqual(str(exception),str(rpc_response.exception())) + + + diff --git a/tests/test_rpc/test_rpcresult.py b/tests/test_rpc/test_rpcresult.py new file mode 100644 index 0000000..9365fcd --- /dev/null +++ b/tests/test_rpc/test_rpcresult.py @@ -0,0 +1,209 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +import unittest + +from uprotocol.proto.ustatus_pb2 import UCode, UStatus +from uprotocol.rpc.rpcresult import RpcResult + + +def getDefault(): + return 5 + + +def multiply_by_2(x): + return RpcResult.success(x * 2) + + +class TestRpcResult(unittest.TestCase): + + def test_isSuccess_on_Success(self): + result = RpcResult.success(2) + self.assertTrue(result.isSuccess()) + + def test_isSuccess_on_Failure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertFalse(result.isSuccess()) + + def test_isFailure_on_Success(self): + result = RpcResult.success(2) + self.assertFalse(result.isFailure()) + + def test_isFailure_on_Failure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertTrue(result.isFailure()) + + def testGetOrElseOnSuccess(self): + result = RpcResult.success(2) + self.assertEqual(2, result.getOrElse(getDefault())) + + def testGetOrElseOnFailure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(getDefault(), result.getOrElse(getDefault)) + + def testGetOrElseOnSuccess_(self): + result = RpcResult.success(2) + self.assertEqual(2, result.getOrElse(5)) + + def testGetOrElseOnFailure_(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(5, result.getOrElse(5)) + + def testSuccessValue_onSuccess(self): + result = RpcResult.success(2) + self.assertEqual(2, result.successValue()) + + def testSuccessValue_onFailure_(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + with self.assertRaises(Exception) as context: + result.successValue() + self.assertEqual(str(context.exception), "Method successValue() called on a Failure instance") + + def testFailureValue_onSuccess(self): + result = RpcResult.success(2) + with self.assertRaises(Exception) as context: + result.failureValue() + self.assertEqual(str(context.exception), "Method failureValue() called on a Success instance") + + def testFailureValue_onFailure_(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + result_value = result.failureValue() + expected_result = UStatus(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(expected_result, result_value) + + def testMapOnSuccess(self): + result = RpcResult.success(2) + mapped = result.map(lambda x: x * 2) + self.assertTrue(mapped.isSuccess()) + self.assertEqual(4, mapped.successValue()) + + def test_map_success_when_function_throws_exception(self): + result = RpcResult.success(2) + mapped = result.map(self.fun_that_throws_exception_for_map) + self.assertTrue(mapped.isFailure()) + self.assertEqual(UCode.UNKNOWN, mapped.failureValue().code) + self.assertEqual("2 went boom", mapped.failureValue().message) + + def test_map_on_failure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + mapped = result.map(lambda x: x * 2) + self.assertTrue(mapped.isFailure()) + expected_status = UStatus(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(expected_status, mapped.failureValue()) + + def test_flat_map_success_when_function_throws_exception(self): + result = RpcResult.success(2) + flat_mapped = result.flatMap(self.fun_that_throws_exception_for_flat_map) + self.assertTrue(flat_mapped.isFailure()) + self.assertEqual(UCode.UNKNOWN, flat_mapped.failureValue().code) + self.assertEqual("2 went boom", flat_mapped.failureValue().message) + + def fun_that_throws_exception_for_flat_map(self, x): + raise ValueError(f"{x} went boom") + + def fun_that_throws_exception_for_map(self, x): + raise ValueError(f"{x} went boom") + + def test_flat_map_on_success(self): + result = RpcResult.success(2) + flat_mapped = result.flatMap(lambda x: RpcResult.success(x * 2)) + self.assertTrue(flat_mapped.isSuccess()) + self.assertEqual(4, flat_mapped.successValue()) + + def test_flat_map_on_failure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + flat_mapped = result.flatMap(lambda x: RpcResult.success(x * 2)) + self.assertTrue(flat_mapped.isFailure()) + expected_status = UStatus(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(expected_status, flat_mapped.failureValue()) + + def test_filter_on_success_that_fails(self): + result = RpcResult.success(2) + filter_result = result.filter(lambda i: i > 5) + self.assertTrue(filter_result.isFailure()) + expected_status = UStatus(code=UCode.FAILED_PRECONDITION, message="filtered out") + self.assertEqual(expected_status, filter_result.failureValue()) + + def test_filter_on_success_that_succeeds(self): + result = RpcResult.success(2) + filter_result = result.filter(lambda i: i < 5) + self.assertTrue(filter_result.isSuccess()) + self.assertEqual(2, filter_result.successValue()) + + def test_filter_on_success_when_function_throws_exception(self): + result = RpcResult.success(2) + filter_result = result.filter(self.predicate_that_throws_exception) + self.assertTrue(filter_result.isFailure()) + expected_status = UStatus(code=UCode.UNKNOWN, message="2 went boom") + self.assertEqual(expected_status, filter_result.failureValue()) + + def predicate_that_throws_exception(self, x): + raise ValueError(f"{x} went boom") + + def test_filter_on_failure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + filter_result = result.filter(lambda i: i > 5) + self.assertTrue(filter_result.isFailure()) + expected_status = UStatus(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(expected_status, filter_result.failureValue()) + + def test_flatten_on_success(self): + result = RpcResult.success(2) + mapped = result.map(multiply_by_2) + mapped_flattened = RpcResult.flatten(mapped) + self.assertTrue(mapped_flattened.isSuccess()) + self.assertEqual(4, mapped_flattened.successValue()) + + def test_flatten_on_success_with_function_that_fails(self): + result = RpcResult.success(2) + mapped = result.map(self.fun_that_throws_exception_for_flat_map) + mapped_flattened = RpcResult.flatten(mapped) + self.assertTrue(mapped_flattened.isFailure()) + expected_status = UStatus(code=UCode.UNKNOWN, message="2 went boom") + self.assertEqual(expected_status, mapped_flattened.failureValue()) + + def test_flatten_on_failure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + mapped = result.map(multiply_by_2) + mapped_flattened = RpcResult.flatten(mapped) + self.assertTrue(mapped_flattened.isFailure()) + expected_status = UStatus(code=UCode.INVALID_ARGUMENT, message="boom") + self.assertEqual(expected_status, mapped_flattened.failureValue()) + + def test_to_string_success(self): + result = RpcResult.success(2) + self.assertEqual("Success(2)", str(result)) + + def test_to_string_failure(self): + result = RpcResult.failure(code=UCode.INVALID_ARGUMENT, message="boom") + expected_string = ("Failure(code: INVALID_ARGUMENT\n" + "message: \"boom\"\n)") + self.assertEqual(expected_string, str(result)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_transport/__init__.py b/tests/test_transport/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_transport/test_builder/__init__.py b/tests/test_transport/test_builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_transport/test_builder/test_uattributesbuilder.py b/tests/test_transport/test_builder/test_uattributesbuilder.py new file mode 100644 index 0000000..0baefbf --- /dev/null +++ b/tests/test_transport/test_builder/test_uattributesbuilder.py @@ -0,0 +1,107 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import unittest +from uprotocol.transport.builder.uattributesbuilder import UAttributesBuilder +from uprotocol.proto.uattributes_pb2 import UPriority, UMessageType +from uprotocol.proto.uri_pb2 import UUri, UAuthority, UEntity +from uprotocol.uri.builder.uresource_builder import UResourceBuilder +from uprotocol.uuid.factory.uuidfactory import Factories + + +def build_sink(): + return UUri(authority=UAuthority(name="vcu.someVin.veh.ultifi.gm.com"), + entity=UEntity(name="petapp.ultifi.gm.com", version_major=1), + resource=UResourceBuilder.for_rpc_response()) + + +def get_uuid(): + return Factories.UPROTOCOL.create() + + +class TestUAttributesBuilder(unittest.TestCase): + + def test_publish(self): + builder = UAttributesBuilder.publish(UPriority.UPRIORITY_CS1) + self.assertIsNotNone(builder) + attributes = builder.build() + self.assertIsNotNone(attributes) + self.assertEqual(UMessageType.UMESSAGE_TYPE_PUBLISH, attributes.type) + self.assertEqual(UPriority.UPRIORITY_CS1, attributes.priority) + + def test_notification(self): + sink = build_sink() + builder = UAttributesBuilder.notification(UPriority.UPRIORITY_CS1, sink) + self.assertIsNotNone(builder) + attributes = builder.build() + self.assertIsNotNone(attributes) + self.assertEqual(UMessageType.UMESSAGE_TYPE_PUBLISH, attributes.type) + self.assertEqual(UPriority.UPRIORITY_CS1, attributes.priority) + self.assertEqual(sink, attributes.sink) + + def test_request(self): + sink = build_sink() + ttl = 1000 + builder = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, sink, ttl) + self.assertIsNotNone(builder) + attributes = builder.build() + self.assertIsNotNone(attributes) + self.assertEqual(UMessageType.UMESSAGE_TYPE_REQUEST, attributes.type) + self.assertEqual(UPriority.UPRIORITY_CS4, attributes.priority) + self.assertEqual(sink, attributes.sink) + self.assertEqual(ttl, attributes.ttl) + + def test_response(self): + sink = build_sink() + req_id = get_uuid() + builder = UAttributesBuilder.response(UPriority.UPRIORITY_CS6, sink, req_id) + self.assertIsNotNone(builder) + attributes = builder.build() + self.assertIsNotNone(attributes) + self.assertEqual(UMessageType.UMESSAGE_TYPE_RESPONSE, attributes.type) + self.assertEqual(UPriority.UPRIORITY_CS6, attributes.priority) + self.assertEqual(sink, attributes.sink) + self.assertEqual(req_id, attributes.reqid) + + def test_build(self): + req_id = get_uuid() + builder = UAttributesBuilder.publish(UPriority.UPRIORITY_CS1).withTtl(1000).withToken("test_token").withSink( + build_sink()).withPermissionLevel(2).withCommStatus(1).withReqId(req_id) + attributes = builder.build() + self.assertIsNotNone(attributes) + self.assertEqual(UMessageType.UMESSAGE_TYPE_PUBLISH, attributes.type) + self.assertEqual(UPriority.UPRIORITY_CS1, attributes.priority) + self.assertEqual(1000, attributes.ttl) + self.assertEqual("test_token", attributes.token) + self.assertEqual(build_sink(), attributes.sink) + self.assertEqual(2, attributes.permission_level) + self.assertEqual(1, attributes.commstatus) + self.assertEqual(req_id, attributes.reqid) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_transport/test_validate/__init__.py b/tests/test_transport/test_validate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_transport/test_validate/test_uattributesvalidator.py b/tests/test_transport/test_validate/test_uattributesvalidator.py new file mode 100644 index 0000000..4ada52d --- /dev/null +++ b/tests/test_transport/test_validate/test_uattributesvalidator.py @@ -0,0 +1,471 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + +import time +import unittest + +from uprotocol.proto.uattributes_pb2 import UPriority +from uprotocol.proto.uri_pb2 import UUri, UAuthority, UEntity +from uprotocol.proto.ustatus_pb2 import UCode +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.transport.builder.uattributesbuilder import UAttributesBuilder +from uprotocol.transport.validate.uattributesvalidator import UAttributesValidator, Validators +from uprotocol.uri.builder.uresource_builder import UResourceBuilder +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer +from uprotocol.uuid.factory.uuidfactory import Factories +from uprotocol.validation.validationresult import ValidationResult + + +def build_sink(): + return UUri(authority=UAuthority(name="vcu.someVin.veh.ultifi.gm.com"), + entity=UEntity(name="petapp.ultifi.gm.com", version_major=1), + resource=UResourceBuilder.for_rpc_response()) + + +class TestUAttributesValidator(unittest.TestCase): + + def test_fetching_validator_for_valid_types(self): + publish = UAttributesValidator.get_validator(UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).build()) + self.assertEqual("UAttributesValidator.Publish", str(publish)) + + request = UAttributesValidator.get_validator( + UAttributesBuilder.request(UPriority.UPRIORITY_CS4, UUri(), 1000).build()) + self.assertEqual("UAttributesValidator.Request", str(request)) + + response = UAttributesValidator.get_validator( + UAttributesBuilder.response(UPriority.UPRIORITY_CS4, UUri(), Factories.UPROTOCOL.create()).build()) + self.assertEqual("UAttributesValidator.Response", str(response)) + + def test_validate_uAttributes_for_publish_message_payload(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).build() + validator = Validators.PUBLISH.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_alls(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withTtl(1000).withSink( + build_sink()).withPermissionLevel(2).withCommStatus(3).withReqId(Factories.UPROTOCOL.create()).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_invalid_type(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS0, build_sink(), + Factories.UPROTOCOL.create()).build() + validator = Validators.PUBLISH.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Wrong Attribute Type [UMESSAGE_TYPE_RESPONSE]", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_invalid_ttl(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withTtl(-1).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid TTL [-1]", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_invalid_sink(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withSink(UUri()).build() + validator = Validators.PUBLISH.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_invalid_permission_level(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withPermissionLevel(-42).build() + validator = Validators.PUBLISH.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Permission Level", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_invalid_communication_status(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withCommStatus(-42).build() + validator = Validators.PUBLISH.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Communication Status Code", status.get_message()) + + # def test_validate_uAttributes_for_publish_message_payload_invalid_request_id(self): + # uuid = java.util.UUID.randomUUID() + # attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withReqId( + # UUID.newBuilder().setMsb(uuid_java.getMostSignificantBits()).setLsb(uuid_java.getLeastSignificantBits()) + # .build()).build() + # + # validator = Validators.PUBLISH.validator() + # status = validator.validate(attributes) + # self.assertTrue(status.is_failure()) + # self.assertEqual("Invalid UUID", status.get_message()) + + def test_validate_uAttributes_for_rpc_request_message_payload(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, build_sink(), 1000).build() + validator = Validators.REQUEST.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_rpc_request_message_payload_alls(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, build_sink(), 1000).withPermissionLevel( + 2).withCommStatus(3).withReqId(Factories.UPROTOCOL.create()).build() + + validator = Validators.REQUEST.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_rpc_request_message_payload_invalid_type(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), + Factories.UPROTOCOL.create()).withTtl(1000).build() + + validator = Validators.REQUEST.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Wrong Attribute Type [UMESSAGE_TYPE_RESPONSE]", status.get_message()) + + def test_validate_uAttributes_for_rpc_request_message_payload_invalid_ttl(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, build_sink(), -1).build() + + validator = Validators.REQUEST.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid TTL [-1]", status.get_message()) + + def test_validate_uAttributes_for_rpc_request_message_payload_invalid_sink(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, UUri(), 1000).build() + + validator = Validators.REQUEST.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validate_uAttributes_for_rpc_request_message_payload_invalid_permission_level(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, build_sink(), 1000).withPermissionLevel( + -42).build() + + validator = Validators.REQUEST.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Permission Level", status.get_message()) + + def test_validate_uAttributes_for_rpc_request_message_payload_invalid_communication_status(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, build_sink(), 1000).withCommStatus(-42).build() + + validator = Validators.REQUEST.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Communication Status Code", status.get_message()) + + # def test_validate_uAttributes_for_rpc_request_message_payload_invalid_request_id(self): + # uuid_java = java.util.UUID.randomUUID() + # attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS4, build_sink(), 1000).withReqId( + # UUID.newBuilder().setMsb(uuid_java.getMostSignificantBits()).setLsb(uuid_java.getLeastSignificantBits()) + # .build()).build() + # + # validator = Validators.REQUEST.validator() + # status = validator.validate(attributes) + # self.assertTrue(status.is_failure()) + # self.assertEqual("Invalid UUID", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), + Factories.UPROTOCOL.create()).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload_alls(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), + Factories.UPROTOCOL.create()).withPermissionLevel(2).withCommStatus( + 3).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload_invalid_type(self): + attributes = UAttributesBuilder.notification(UPriority.UPRIORITY_CS4, build_sink()).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Wrong Attribute Type [UMESSAGE_TYPE_PUBLISH],Missing correlationId", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload_invalid_ttl(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), + Factories.UPROTOCOL.create()).withTtl(-1).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid TTL [-1]", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload_missing_sink_and_missing_requestId(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, UUri(), UUID()).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Missing Sink,Missing correlationId", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload_invalid_permission_level(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), + Factories.UPROTOCOL.create()).withPermissionLevel(-42).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Permission Level", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload_invalid_communication_status(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), + Factories.UPROTOCOL.create()).withCommStatus(-42).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Communication Status Code", status.get_message()) + + def test_validate_uAttributes_for_rpc_response_message_payload_missing_request_id(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), UUID()).build() + + validator = Validators.RESPONSE.validator() + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Missing correlationId", status.get_message()) + + # def test_validate_uAttributes_for_rpc_response_message_payload_invalid_request_id(self): + # uuid_java = java.util.UUID.randomUUID() + # reqid = UUID.newBuilder().setMsb(uuid_java.getMostSignificantBits()).setLsb( + # uuid_java.getLeastSignificantBits()).build() + # attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS4, build_sink(), reqid).build() + # + # validator = Validators.RESPONSE.validator() + # status = validator.validate(attributes) + # self.assertTrue(status.is_failure()) + # self.assertEqual(f"Invalid correlationId [{reqid}]", status.get_message()) + + # ---- + def test_validate_uAttributes_for_publish_message_payload_not_expired(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).build() + + validator = Validators.PUBLISH.validator() + status = validator.is_expired(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_not_expired_withTtl_zero(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withTtl(0).build() + + validator = Validators.PUBLISH.validator() + status = validator.is_expired(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_not_expired_withTtl(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withTtl(10000).build() + + validator = Validators.PUBLISH.validator() + status = validator.is_expired(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validate_uAttributes_for_publish_message_payload_expired_withTtl(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withTtl(1).build() + + time.sleep(0.8) + + validator = Validators.PUBLISH.validator() + status = validator.is_expired(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Payload is expired", status.get_message()) + + # ---- + + def test_validating_publish_invalid_ttl_attribute(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withTtl(-1).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_ttl(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid TTL [-1]", status.get_message()) + + def test_validating_valid_ttl_attribute(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withTtl(100).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_ttl(attributes) + self.assertEqual(ValidationResult.success(), status) + + def test_validating_invalid_sink_attribute(self): + uri = LongUriSerializer().deserialize("//") + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withSink(uri).build() + validator = Validators.PUBLISH.validator() + status = validator.validate_sink(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validating_valid_sink_attribute(self): + uri = UUri(authority=UAuthority(name="vcu.someVin.veh.ultifi.gm.com"), + entity=UEntity(name="petapp.ultifi.gm.com", version_major=1), + resource=UResourceBuilder.for_rpc_response()) + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withSink(uri).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_sink(attributes) + self.assertEqual(ValidationResult.success(), status) + + # def test_validating_invalid_ReqId_attribute(self): + # uuid_java = java.util.UUID.randomUUID() + # + # attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).with_req_id( + # UUID.newBuilder().setMsb(uuid_java.getMostSignificantBits()).setLsb(uuid_java.getLeastSignificantBits()) + # .build()).build() + # + # validator = Validators.PUBLISH.validator() + # status = validator.validate_req_id(attributes) + # self.assertTrue(status.is_failure()) + # self.assertEqual("Invalid UUID", status.get_message()) + + def test_validating_valid_ReqId_attribute(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withReqId( + Factories.UPROTOCOL.create()).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_req_id(attributes) + self.assertEqual(ValidationResult.success(), status) + + def test_validating_invalid_PermissionLevel_attribute(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withPermissionLevel(-1).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_permission_level(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Permission Level", status.get_message()) + + def test_validating_valid_PermissionLevel_attribute(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withPermissionLevel(3).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_permission_level(attributes) + self.assertEqual(ValidationResult.success(), status) + + def test_validating_valid_PermissionLevel_attribute_invalid(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withPermissionLevel(0).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_permission_level(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Permission Level", status.get_message()) + + def test_validating_invalid_commstatus_attribute(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withCommStatus(100).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_comm_status(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid Communication Status Code", status.get_message()) + + def test_validating_valid_commstatus_attribute(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withCommStatus(UCode.ABORTED).build() + + validator = Validators.PUBLISH.validator() + status = validator.validate_comm_status(attributes) + self.assertEqual(ValidationResult.success(), status) + + def test_validating_request_message_types(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS6, build_sink(), 100).build() + + validator = UAttributesValidator.get_validator(attributes) + self.assertEqual("UAttributesValidator.Request", str(validator)) + status = validator.validate(attributes) + self.assertTrue(status.is_success()) + self.assertEqual("", status.get_message()) + + def test_validating_request_validator_with_wrong_messagetype(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS6).build() + + validator = Validators.REQUEST.validator() + self.assertEqual("UAttributesValidator.Request", str(validator)) + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Wrong Attribute Type [UMESSAGE_TYPE_PUBLISH],Missing TTL,Missing Sink", status.get_message()) + + def test_validating_request_validator_with_wrong_bad_ttl(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS6, + LongUriSerializer().deserialize("/hartley/1/rpc.response"), -1).build() + + validator = Validators.REQUEST.validator() + self.assertEqual("UAttributesValidator.Request", str(validator)) + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid TTL [-1]", status.get_message()) + + def test_validating_response_validator_with_wrong_bad_ttl(self): + attributes = UAttributesBuilder.response(UPriority.UPRIORITY_CS6, + LongUriSerializer().deserialize("/hartley/1/rpc.response"), + Factories.UPROTOCOL.create()).withTtl(-1).build() + + validator = Validators.RESPONSE.validator() + self.assertEqual("UAttributesValidator.Response", str(validator)) + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid TTL [-1]", status.get_message()) + + def test_validating_publish_validator_with_wrong_messagetype(self): + attributes = UAttributesBuilder.request(UPriority.UPRIORITY_CS6, build_sink(), 1000).build() + validator = Validators.PUBLISH.validator() + self.assertEqual("UAttributesValidator.Publish", str(validator)) + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Wrong Attribute Type [UMESSAGE_TYPE_REQUEST]", status.get_message()) + + def test_validating_response_validator_with_wrong_messagetype(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS6).build() + + validator = Validators.RESPONSE.validator() + self.assertEqual("UAttributesValidator.Response", str(validator)) + status = validator.validate(attributes) + self.assertTrue(status.is_failure()) + self.assertEqual("Wrong Attribute Type [UMESSAGE_TYPE_PUBLISH],Missing Sink,Missing correlationId", + status.get_message()) + + def test_validating_request_containing_token(self): + attributes = UAttributesBuilder.publish(UPriority.UPRIORITY_CS0).withToken("null").build() + + validator = UAttributesValidator.get_validator(attributes) + self.assertEqual("UAttributesValidator.Publish", str(validator)) + status = validator.validate(attributes) + self.assertEqual(ValidationResult.success(), status) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_uri/__init__.py b/tests/test_uri/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_uri/test_serializer/__init__.py b/tests/test_uri/test_serializer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_uri/test_serializer/test_longuriserializer.py b/tests/test_uri/test_serializer/test_longuriserializer.py new file mode 100644 index 0000000..7b89c8f --- /dev/null +++ b/tests/test_uri/test_serializer/test_longuriserializer.py @@ -0,0 +1,686 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +import unittest + +from uprotocol.proto.uri_pb2 import UEntity, UUri, UAuthority, UResource +from uprotocol.uri.builder.uresource_builder import UResourceBuilder +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer +from uprotocol.uri.validator.urivalidator import UriValidator + + +class TestLongUriSerializer(unittest.TestCase): + + def test_using_the_serializers(self): + uri = UUri(entity=UEntity(name="hartley"), resource=UResourceBuilder.for_rpc_request("raise")) + + str_uri = LongUriSerializer().serialize(uri) + self.assertEqual("/hartley//rpc.raise", str_uri) + uri2 = LongUriSerializer().deserialize(str_uri) + self.assertEqual(uri, uri2) + + def test_parse_protocol_uri_when_is_null(self): + uri = LongUriSerializer().deserialize(None) + self.assertTrue(UriValidator.is_empty(uri)) + self.assertFalse(UriValidator.is_resolved(uri)) + self.assertFalse(UriValidator.is_long_form(uri)) + + def test_parse_protocol_uri_when_is_empty_string(self): + uri = '' + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_empty(uuri)) + uri2 = LongUriSerializer().serialize(None) + self.assertTrue(len(uri2) == 0) + + def test_parse_protocol_uri_with_schema_and_slash(self): + uri = "/" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(uuri.HasField('authority')) + self.assertTrue(UriValidator.is_empty(uuri)) + self.assertFalse(uuri.HasField('resource')) + self.assertFalse(uuri.HasField('entity')) + uri2 = LongUriSerializer().serialize(UUri()) + self.assertTrue(len(uri2) == 0) + + def test_parse_protocol_uri_with_schema_and_double_slash(self): + uri = "//" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(uuri.HasField('authority')) + self.assertFalse(uuri.HasField('resource')) + self.assertFalse(uuri.HasField('entity')) + self.assertTrue(UriValidator.is_empty(uuri)) + + def test_parse_protocol_uri_with_schema_and_3_slash_and_something(self): + uri = "///body.access" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(uuri.HasField('authority')) + self.assertFalse(uuri.HasField('resource')) + self.assertFalse(uuri.HasField('entity')) + self.assertTrue(UriValidator.is_empty(uuri)) + self.assertNotEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + + def test_parse_protocol_uri_with_schema_and_4_slash_and_something(self): + uri = "////body.access" + uuri = LongUriSerializer().deserialize(uri) + + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertFalse(uuri.HasField('resource')) + self.assertFalse(uuri.HasField('entity')) + self.assertTrue(len(uuri.entity.name) == 0) + self.assertEqual(0, uuri.entity.version_major) + + def test_parse_protocol_uri_with_schema_and_5_slash_and_something(self): + uri = "/////body.access" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertFalse(uuri.HasField('resource')) + self.assertFalse(uuri.HasField('entity')) + self.assertTrue(UriValidator.is_empty(uuri)) + + def test_parse_protocol_uri_with_schema_and_6_slash_and_something(self): + uri = "//////body.access" + uuri = LongUriSerializer().deserialize(uri) + + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertTrue(UriValidator.is_empty(uuri)) + + def test_parse_protocol_uri_with_local_service_no_version(self): + uri = "/body.access" + uuri = LongUriSerializer().deserialize(uri) + + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual(0, uuri.entity.version_minor) + self.assertFalse(uuri.HasField('resource')) + + def test_parse_protocol_uri_with_local_service_with_version(self): + uri = "/body.access/1" + uuri = LongUriSerializer().deserialize(uri) + + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(1, uuri.entity.version_major) + self.assertFalse(uuri.HasField('resource')) + + def test_parse_protocol_uri_with_local_service_no_version_with_resource_name_only(self): + uri = "/body.access//door" + uuri = LongUriSerializer().deserialize(uri) + + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual(0, uuri.entity.version_minor) + self.assertEqual("door", uuri.resource.name) + self.assertTrue(len(uuri.resource.instance) == 0) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_local_service_with_version_with_resource_name_only(self): + uri = "/body.access/1/door" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertTrue(len(uuri.resource.instance) == 0) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_local_service_no_version_with_resource_with_instance(self): + uri = "/body.access//door.front_left" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_local_service_with_version_with_resource_with_getMessage(self): + uri = "/body.access/1/door.front_left" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_local_service_no_version_with_resource_with_instance_and_getMessage(self): + uri = "/body.access//door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertFalse(len(uuri.resource.message) == 0) + self.assertEqual("Door", uuri.resource.message) + + def test_parse_protocol_uri_with_local_service_with_version_with_resource_with_instance_and_getMessage(self): + uri = "/body.access/1/door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertFalse(len(uuri.resource.message) == 0) + self.assertEqual("Door", uuri.resource.message) + + def test_parse_protocol_rpc_uri_with_local_service_no_version(self): + uri = "/petapp//rpc.response" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("petapp", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("rpc", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("response", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_rpc_uri_with_local_service_with_version(self): + uri = "/petapp/1/rpc.response" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("petapp", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("rpc", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("response", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_remote_service_only_device_and_domain(self): + uri = "//VCU.MY_CAR_VIN" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + + def test_parse_protocol_uri_with_remote_service_only_device_and_cloud_domain(self): + uri = "//cloud.uprotocol.example.com" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("cloud.uprotocol.example.com", uuri.authority.name) + self.assertFalse(uuri.HasField('entity')) + self.assertFalse(uuri.HasField('resource')) + + def test_parse_protocol_uri_with_remote_service_no_version(self): + uri = "//VCU.MY_CAR_VIN/body.access" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertFalse(uuri.HasField('resource')) + + def test_parse_protocol_uri_with_remote_cloud_service_no_version(self): + uri = "//cloud.uprotocol.example.com/body.access" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("cloud.uprotocol.example.com", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertFalse(uuri.HasField('resource')) + + def test_parse_protocol_uri_with_remote_service_with_version(self): + uri = "//VCU.MY_CAR_VIN/body.access/1" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertFalse(uuri.HasField('resource')) + + def test_parse_protocol_uri_with_remote_cloud_service_with_version(self): + uri = "//cloud.uprotocol.example.com/body.access/1" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("cloud.uprotocol.example.com", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertFalse(uuri.HasField('resource')) + + def test_parse_protocol_uri_with_remote_service_no_version_with_resource_name_only(self): + uri = "//VCU.MY_CAR_VIN/body.access//door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertTrue(len(uuri.resource.instance) == 0) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_remote_cloud_service_no_version_with_resource_name_only(self): + uri = "//cloud.uprotocol.example.com/body.access//door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("cloud.uprotocol.example.com", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertTrue(len(uuri.resource.instance) == 0) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_remote_service_with_version_with_resource_name_only(self): + uri = "//VCU.MY_CAR_VIN/body.access/1/door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertTrue(len(uuri.resource.instance) == 0) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_remote_service_cloud_with_version_with_resource_name_only(self): + uri = "//cloud.uprotocol.example.com/body.access/1/door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("cloud.uprotocol.example.com", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertTrue(len(uuri.resource.instance) == 0) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_remote_service_no_version_with_resource_and_instance_no_getMessage(self): + uri = "//VCU.MY_CAR_VIN/body.access//door.front_left" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_remote_service_with_version_with_resource_and_instance_no_getMessage(self): + uri = "//VCU.MY_CAR_VIN/body.access/1/door.front_left" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_uri_with_remote_service_no_version_with_resource_and_instance_and_getMessage(self): + uri = "//VCU.MY_CAR_VIN/body.access//door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertFalse(len(uuri.resource.message) == 0) + self.assertEqual("Door", uuri.resource.message) + + def test_parse_protocol_uri_with_remote_cloud_service_no_version_with_resource_and_instance_and_getMessage(self): + uri = "//cloud.uprotocol.example.com/body.access//door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("cloud.uprotocol.example.com", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertFalse(len(uuri.resource.message) == 0) + self.assertEqual("Door", uuri.resource.message) + + def test_parse_protocol_uri_with_remote_service_with_version_with_resource_and_instance_and_getMessage(self): + uri = "//VCU.MY_CAR_VIN/body.access/1/door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU.MY_CAR_VIN", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertFalse(len(uuri.resource.message) == 0) + self.assertEqual("Door", uuri.resource.message) + + def test_parse_protocol_uri_with_remote_cloud_service_with_version_with_resource_and_instance_and_getMessage(self): + uri = "//cloud.uprotocol.example.com/body.access/1/door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("cloud.uprotocol.example.com", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertFalse(len(uuri.resource.message) == 0) + self.assertEqual("Door", uuri.resource.message) + + def test_parse_protocol_uri_with_remote_service_with_version_with_resource_with_message_device_no_domain(self): + uri = "//VCU/body.access/1/door.front_left" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("VCU", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("body.access", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_rpc_uri_with_remote_service_no_version(self): + uri = "//bo.cloud/petapp//rpc.response" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("bo.cloud", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("petapp", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("rpc", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("response", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_parse_protocol_rpc_uri_with_remote_service_with_version(self): + uri = "//bo.cloud/petapp/1/rpc.response" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertFalse(len(uuri.authority.name) == 0) + self.assertEqual("bo.cloud", uuri.authority.name) + self.assertFalse(len(uuri.entity.name) == 0) + self.assertEqual("petapp", uuri.entity.name) + self.assertNotEqual(0, uuri.entity.version_major) + self.assertEqual(1, uuri.entity.version_major) + self.assertEqual("rpc", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance) == 0) + self.assertEqual("response", uuri.resource.instance) + self.assertTrue(len(uuri.resource.message) == 0) + + def test_build_protocol_uri_from__uri_when__uri_isnull(self): + uprotocol_uri = LongUriSerializer().serialize(None) + self.assertEqual('', uprotocol_uri) + + def test_build_protocol_uri_from__uri_when__uri_isEmpty(self): + uuri = UUri() + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual('', uprotocol_uri) + + def test_build_protocol_uri_from__uri_when__uri_has_empty_use(self): + use = UEntity() + uuri = UUri(authority=UAuthority(), entity=use, resource=UResource(name="door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/////door", uprotocol_uri) + + def test_build_protocol_uri_from__uri_when__uri_has_local_authority_service_no_version(self): + uuri = UUri(entity=UEntity(name="body.access")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access", uprotocol_uri) + + def test_build_protocol_uri_from__uri_when__uri_has_local_authority_service_and_version(self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(entity=use, resource=UResource()) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access/1", uprotocol_uri) + + def test_build_protocol_uri_from__uri_when__uri_has_local_authority_service_no_version_with_resource(self): + use = UEntity(name="body.access") + uuri = UUri(entity=use, resource=UResource(name="door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access//door", uprotocol_uri) + + def test_build_protocol_uri_from__uri_when__uri_has_local_authority_service_and_version_with_resource(self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(entity=use, resource=UResource(name="door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access/1/door", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_local_authority_service_no_version_with_resource_with_instance_no_getMessage( + self): + use = UEntity(name="body.access") + uuri = UUri(entity=use, resource=UResource(name="door", instance="front_left")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access//door.front_left", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_local_authority_service_and_version_with_resource_with_instance_no_getMessage( + self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(entity=use, resource=UResource(name="door", instance="front_left")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access/1/door.front_left", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_local_authority_service_no_version_with_resource_with_instance_with_getMessage( + self): + use = UEntity(name="body.access") + uuri = UUri(entity=use, resource=UResource(name="door", instance="front_left", message="Door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access//door.front_left#Door", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_local_authority_service_and_version_with_resource_with_instance_with_getMessage( + self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(entity=use, resource=UResource(name="door", instance="front_left", message="Door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("/body.access/1/door.front_left#Door", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_no_version(self): + use = UEntity(name="body.access") + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_and_version(self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access/1", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_cloud_authority_service_and_version(self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(authority=UAuthority(name="cloud.uprotocol.example.com"), entity=use) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//cloud.uprotocol.example.com/body.access/1", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_and_version_with_resource(self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use, resource=UResource(name="door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access/1/door", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_no_version_with_resource(self): + use = UEntity(name="body.access") + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use, resource=UResource(name="door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access//door", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_and_version_with_resource_with_instance_no_getMessage( + self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use, + resource=UResource(name="door", instance="front_left")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access/1/door.front_left", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_cloud_authority_service_and_version_with_resource_with_instance_no_getMessage( + self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(authority=UAuthority(name="cloud.uprotocol.example.com"), entity=use, + resource=UResource(name="door", instance="front_left")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//cloud.uprotocol.example.com/body.access/1/door.front_left", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_no_version_with_resource_with_instance_no_getMessage( + self): + use = UEntity(name="body.access") + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use, + resource=UResource(name="door", instance="front_left")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access//door.front_left", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_and_version_with_resource_with_instance_and_getMessage( + self): + use = UEntity(name="body.access", version_major=1) + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use, + resource=UResource(name="door", instance="front_left", message="Door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access/1/door.front_left#Door", uprotocol_uri) + + def test_build_protocol_uri_from_uri_when_uri_has_remote_authority_service_no_version_with_resource_with_instance_and_getMessage( + self): + use = UEntity(name="body.access") + uuri = UUri(authority=UAuthority(name="vcu.my_car_vin"), entity=use, + resource=UResource(name="door", instance="front_left", message="Door")) + uprotocol_uri = LongUriSerializer().serialize(uuri) + self.assertEqual("//vcu.my_car_vin/body.access//door.front_left#Door", uprotocol_uri) + + def test_build_protocol_uri_for_source_part_of_rpc_request_where_source_is_local(self): + use = UEntity(name="petapp", version_major=1) + resource = UResource(name="rpc", instance="response") + uprotocol_uri = LongUriSerializer().serialize(UUri(entity=use, resource=resource)) + self.assertEqual("/petapp/1/rpc.response", uprotocol_uri) + + def test_build_protocol_uri_for_source_part_of_rpc_request_where_source_is_remote(self): + uAuthority = UAuthority(name="cloud.uprotocol.example.com") + use = UEntity(name="petapp") + resource = UResource(name="rpc", instance="response") + + uprotocol_uri = LongUriSerializer().serialize(UUri(authority=uAuthority, entity=use, resource=resource)) + self.assertEqual("//cloud.uprotocol.example.com/petapp//rpc.response", uprotocol_uri) + + def test_build_protocol_uri_from_uri_parts_when_uri_has_remote_authority_service_and_version_with_resource(self): + u_authority = UAuthority(name="vcu.my_car_vin") + use = UEntity(name="body.access", version_major=1) + resource = UResource(name="door") + uprotocol_uri = LongUriSerializer().serialize(UUri(authority=u_authority, entity=use, resource=resource)) + self.assertEqual("//vcu.my_car_vin/body.access/1/door", uprotocol_uri) + + def test_custom_scheme_no_scheme(self): + u_authority = UAuthority(name="vcu.my_car_vin") + use = UEntity(name="body.access", version_major=1) + resource = UResource(name="door") + ucustom_uri = LongUriSerializer().serialize(UUri(authority=u_authority, entity=use, resource=resource)) + self.assertEqual("//vcu.my_car_vin/body.access/1/door", ucustom_uri) + + def test_parse_local_protocol_uri_with_custom_scheme(self): + uri = "custom:/body.access//door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertFalse(UriValidator.is_remote(uuri.authority)) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertFalse(len(uuri.resource.instance.strip()) == 0) + self.assertEqual("front_left", uuri.resource.instance) + self.assertFalse(len(uuri.resource.message.strip()) == 0) + self.assertEqual("Door", uuri.resource.message) + + def test_parse_remote_protocol_uri_with_custom_scheme(self): + uri = "custom://vcu.vin/body.access//door.front_left#Door" + uri2 = "//vcu.vin/body.access//door.front_left#Door" + uuri = LongUriSerializer().deserialize(uri) + self.assertTrue(UriValidator.is_remote(uuri.authority)) + self.assertEqual("vcu.vin", uuri.authority.name) + self.assertEqual("body.access", uuri.entity.name) + self.assertEqual(0, uuri.entity.version_major) + self.assertEqual("door", uuri.resource.name) + self.assertEqual("front_left", uuri.resource.instance) + self.assertEqual("Door", uuri.resource.message) + self.assertEqual(uri2, LongUriSerializer().serialize(uuri)) + + def test_deserialize_long_and_micro_passing_null(self): + uri = LongUriSerializer().build_resolved(None, None) + self.assertTrue(uri is not None) + self.assertEqual("", LongUriSerializer().serialize(uri)) + + def test_deserialize_long_and_micro_passing_null_long_uri_empty_byte_array(self): + uri = LongUriSerializer().build_resolved(None, bytearray()) + self.assertTrue(uri is not None) + self.assertEqual("", LongUriSerializer().serialize(uri)) + + def test_deserialize_long_and_micro_passing_nullempty_long_uri_null_byte_array(self): + uri = LongUriSerializer().build_resolved("", None) + self.assertTrue(uri is not None) + self.assertEqual("", LongUriSerializer().serialize(uri)) + + def test_deserialize_long_and_micro_passing_empty_long_uri_empty_byte_array(self): + uri = LongUriSerializer().build_resolved("", bytearray()) + self.assertTrue(uri is not None) + self.assertEqual("", LongUriSerializer().serialize(uri)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_uri/test_serializer/test_microuriserializer.py b/tests/test_uri/test_serializer/test_microuriserializer.py new file mode 100644 index 0000000..2b66f8e --- /dev/null +++ b/tests/test_uri/test_serializer/test_microuriserializer.py @@ -0,0 +1,156 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + +import socket +import unittest + +from uprotocol.proto.uri_pb2 import UEntity, UUri, UAuthority, UResource +from uprotocol.uri.builder.uresource_builder import UResourceBuilder +from uprotocol.uri.serializer.microuriserializer import MicroUriSerializer +from uprotocol.uri.validator.urivalidator import UriValidator + + +class TestMicroUriSerializer(unittest.TestCase): + + def test_empty(self): + bytes_uuri = MicroUriSerializer().serialize(UUri()) + self.assertEqual(len(bytes_uuri), 0) + uri2 = MicroUriSerializer().deserialize(bytes_uuri) + self.assertTrue(UriValidator.is_empty(uri2)) + + def test_null(self): + bytes_uuri = MicroUriSerializer().serialize(None) + self.assertEqual(len(bytes_uuri), 0) + uri2 = MicroUriSerializer().deserialize(None) + self.assertTrue(UriValidator.is_empty(uri2)) + + def test_serialize_uri(self): + uri = UUri(entity=UEntity(id=29999, version_major=254), resource=UResource(id=19999)) + bytes_uuri = MicroUriSerializer().serialize(uri) + uri2 = MicroUriSerializer().deserialize(bytes_uuri) + self.assertEqual(uri, uri2) + + def test_serialize_remote_uri_without_address(self): + uri = UUri(authority=UAuthority(name="vcu.vin"), entity=UEntity(id=29999, version_major=254), + resource=UResourceBuilder.for_rpc_response()) + bytes_uuri = MicroUriSerializer().serialize(uri) + self.assertTrue(len(bytes_uuri) == 0) + + def test_serialize_uri_missing_ids(self): + uri = UUri(entity=UEntity(name="hartley"), resource=UResourceBuilder.for_rpc_response()) + bytes_uuri = MicroUriSerializer().serialize(uri) + self.assertTrue(len(bytes_uuri) == 0) + + def test_serialize_uri_missing_resource_id(self): + uri = UUri(entity=UEntity(name="hartley")) + bytes_uuri = MicroUriSerializer().serialize(uri) + self.assertTrue(len(bytes_uuri) == 0) + + def test_deserialize_bad_microuri_length(self): + badMicroUUri = bytes([0x1, 0x0, 0x0, 0x0, 0x0]) + uuri = MicroUriSerializer().deserialize(badMicroUUri) + self.assertTrue(UriValidator.is_empty(uuri)) + + badMicroUUri = bytes([0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]) + uuri = MicroUriSerializer().deserialize(badMicroUUri) + self.assertTrue(UriValidator.is_empty(uuri)) + + def test_deserialize_bad_microuri_not_version_1(self): + badMicroUUri = bytes([0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]) + uuri = MicroUriSerializer().deserialize(badMicroUUri) + self.assertTrue(UriValidator.is_empty(uuri)) + + def test_deserialize_bad_microuri_not_valid_address_type(self): + badMicroUUri = bytes([0x1, 0x5, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]) + uuri = MicroUriSerializer().deserialize(badMicroUUri) + self.assertTrue(UriValidator.is_empty(uuri)) + + def test_deserialize_bad_microuri_valid_address_type_invalid_length(self): + badMicroUUri = bytes([0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]) + uuri = MicroUriSerializer().deserialize(badMicroUUri) + self.assertTrue(UriValidator.is_empty(uuri)) + + badMicroUUri = bytes([0x1, 0x1, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]) + uuri = MicroUriSerializer().deserialize(badMicroUUri) + self.assertTrue(UriValidator.is_empty(uuri)) + + badMicroUUri = bytes([0x1, 0x2, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0]) + uuri = MicroUriSerializer().deserialize(badMicroUUri) + self.assertTrue(UriValidator.is_empty(uuri)) + + def test_serialize_good_ipv4_based_authority(self): + uri = UUri(authority=UAuthority(ip=bytes(socket.inet_pton(socket.AF_INET, "10.0.3.3"))), + entity=UEntity(id=29999, version_major=254), resource=UResourceBuilder.for_rpc_request_with_id(99)) + bytes_uuri = MicroUriSerializer().serialize(uri) + uri2 = MicroUriSerializer().deserialize(bytes_uuri) + self.assertTrue(len(bytes_uuri) > 0) + self.assertTrue(UriValidator.is_micro_form(uri)) + self.assertTrue(UriValidator.is_micro_form(uri2)) + self.assertEqual(str(uri), str(uri2)) + self.assertTrue(uri == uri2) + + def test_serialize_good_ipv6_based_authority(self): + uri = UUri(authority=UAuthority( + ip=bytes(socket.inet_pton(socket.AF_INET6, "2001:0db8:85a3:0000:0000:8a2e:0370:7334"))), + entity=UEntity(id=29999, version_major=254), resource=UResource(id=19999)) + bytes_uuri = MicroUriSerializer().serialize(uri) + uri2 = MicroUriSerializer().deserialize(bytes_uuri) + self.assertTrue(UriValidator.is_micro_form(uri)) + self.assertTrue(len(bytes_uuri) > 0) + self.assertTrue(uri == uri2) + + def test_serialize_id_based_authority(self): + size = 13 + byteArray = bytearray(i for i in range(size)) + uri = UUri(authority=UAuthority(id=bytes(byteArray)), entity=UEntity(id=29999, version_major=254), + resource=UResource(id=19999)) + bytes_uuri = MicroUriSerializer().serialize(uri) + uri2 = MicroUriSerializer().deserialize(bytes_uuri) + self.assertTrue(UriValidator.is_micro_form(uri)) + self.assertTrue(len(bytes_uuri) > 0) + self.assertTrue(uri == uri2) + + def test_serialize_bad_length_ip_based_authority(self): + byteArray = bytes([127, 1, 23, 123, 12, 6]) + uri = UUri(authority=UAuthority(ip=bytes(byteArray)), entity=UEntity(id=29999, version_major=254), + resource=UResource(id=19999)) + bytes_uuri = MicroUriSerializer().serialize(uri) + self.assertTrue(len(bytes_uuri) == 0) + + def test_serialize_id_size_255_based_authority(self): + size = 129 + byteArray = bytes(i for i in range(size)) + uri = UUri(authority=UAuthority(id=bytes(byteArray)), entity=UEntity(id=29999, version_major=254), + resource=UResourceBuilder.for_rpc_request_with_id(99)) + bytes_uuri = MicroUriSerializer().serialize(uri) + self.assertEqual(len(bytes_uuri), 9 + size) + uri2 = MicroUriSerializer().deserialize(bytes_uuri) + self.assertTrue(UriValidator.is_micro_form(uri)) + self.assertTrue(uri == uri2) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_uri/test_serializer/test_uriserializer.py b/tests/test_uri/test_serializer/test_uriserializer.py new file mode 100644 index 0000000..5f0ef32 --- /dev/null +++ b/tests/test_uri/test_serializer/test_uriserializer.py @@ -0,0 +1,72 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +import unittest + +from uprotocol.proto.uri_pb2 import UAuthority, UEntity, UResource, UUri +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer +from uprotocol.uri.serializer.microuriserializer import MicroUriSerializer +from uprotocol.uri.validator.urivalidator import UriValidator + + +class TestUriSerializer(unittest.TestCase): + + def test_build_resolved_valid_long_micro_uri(self): + long_uuri = UUri(authority=UAuthority(name="testauth"), entity=UEntity(name="neelam"), + resource=UResource(name="rpc", instance="response")) + micro_uuri = UUri(entity=UEntity(id=29999, version_major=254), resource=UResource(id=39999)) + microuri = MicroUriSerializer().serialize(micro_uuri) + longuri = LongUriSerializer().serialize(long_uuri) + resolved_uuri = LongUriSerializer().build_resolved(longuri, microuri) + self.assertTrue(resolved_uuri) + self.assertFalse(UriValidator.is_empty(resolved_uuri)) + self.assertEqual("testauth", resolved_uuri.authority.name) + self.assertEqual("neelam", resolved_uuri.entity.name) + self.assertEqual(29999, resolved_uuri.entity.id) + self.assertEqual(254, resolved_uuri.entity.version_major) + self.assertEqual("rpc", resolved_uuri.resource.name) + self.assertEqual("response", resolved_uuri.resource.instance) + self.assertEqual(39999, resolved_uuri.resource.id) + + def test_build_resolved_null_long_null_micro_uri(self): + result = MicroUriSerializer().build_resolved(None, None) + self.assertTrue(result) + self.assertTrue(UriValidator.is_empty(result)) + + def test_build_resolved_null_long_micro_uri(self): + result = MicroUriSerializer().build_resolved(None, bytes()) + self.assertTrue(result) + self.assertTrue(UriValidator.is_empty(result)) + + def test_build_resolved_valid_long_null_micro_uri(self): + result = MicroUriSerializer().build_resolved("", bytes()) + self.assertTrue(result) + self.assertTrue(UriValidator.is_empty(result)) + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_uri/test_validator/__init__.py b/tests/test_uri/test_validator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_uri/test_validator/test_urivalidator.py b/tests/test_uri/test_validator/test_urivalidator.py new file mode 100644 index 0000000..c674235 --- /dev/null +++ b/tests/test_uri/test_validator/test_urivalidator.py @@ -0,0 +1,360 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + + +import json +import os +import unittest + +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer +from uprotocol.uri.validator.urivalidator import UriValidator +from uprotocol.validation.validationresult import ValidationResult +from uprotocol.proto.uri_pb2 import UUri, UEntity, UResource, UAuthority + + +class TestUriValidator(unittest.TestCase): + + def test_validate_blank_uri(self): + uri = LongUriSerializer().deserialize(None) + status = UriValidator.validate(uri) + self.assertTrue(UriValidator.is_empty(uri)) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validate_uri_with_no_entity_getName(self): + uri = LongUriSerializer().deserialize("//") + status = UriValidator.validate(uri) + self.assertTrue(UriValidator.is_empty(uri)) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validate_uri_with_getEntity(self): + uri = LongUriSerializer().deserialize("/neelam") + status = UriValidator.validate(uri) + self.assertTrue(ValidationResult.success().__eq__(status)) + + + def test_validate_with_malformed_uri(self): + uri = LongUriSerializer().deserialize("neelam") + status = UriValidator.validate(uri) + self.assertTrue(UriValidator.is_empty(uri)) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validate_with_blank_uentity_name_uri(self): + status = UriValidator.validate(UUri()) + self.assertTrue(status.is_failure()) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validateRpcMethod_with_valid_uri(self): + uri = LongUriSerializer().deserialize("/neelam//rpc.echo") + status = UriValidator.validate_rpc_method(uri) + self.assertTrue(ValidationResult.success().__eq__(status)) + + def test_validateRpcMethod_with_invalid_uri(self): + uri = LongUriSerializer().deserialize("/neelam/echo") + status = UriValidator.validate_rpc_method(uri) + self.assertTrue(status.is_failure()) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validateRpcMethod_with_malformed_uri(self): + uri = LongUriSerializer().deserialize("neelam") + status = UriValidator.validate_rpc_method(uri) + self.assertTrue(UriValidator.is_empty(uri)) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validateRpcResponse_with_valid_uri(self): + uri = LongUriSerializer().deserialize("/neelam//rpc.response") + status = UriValidator.validate_rpc_response(uri) + self.assertTrue(ValidationResult.success().__eq__(status)) + + + def test_validateRpcResponse_with_malformed_uri(self): + uri = LongUriSerializer().deserialize("neelam") + status = UriValidator.validate_rpc_response(uri) + self.assertTrue(UriValidator.is_empty(uri)) + self.assertEqual("Uri is empty.", status.get_message()) + + def test_validateRpcResponse_with_rpc_type(self): + uri = LongUriSerializer().deserialize("/neelam//dummy.wrong") + status = UriValidator.validate_rpc_response(uri) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid RPC response type.", status.get_message()) + + def test_validateRpcResponse_with_invalid_rpc_response_type(self): + uri = LongUriSerializer().deserialize("/neelam//rpc.wrong") + status = UriValidator.validate_rpc_response(uri) + self.assertTrue(status.is_failure()) + self.assertEqual("Invalid RPC response type.", status.get_message()) + + def test_topic_uri_with_version_when_it_is_valid_remote(self): + uri = "//VCU.MY_CAR_VIN/body.access/1/door.front_left#Door" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_topic_uri_no_version_when_it_is_valid_remote(self): + uri = "//VCU.MY_CAR_VIN/body.access//door.front_left#Door" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_topic_uri_with_version_when_it_is_valid_local(self): + uri = "/body.access/1/door.front_left#Door" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_topic_uri_no_version_when_it_is_valid_local(self): + uri = "/body.access//door.front_left#Door" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_topic_uri_invalid_when_uri_has_schema_only(self): + uri = ":" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_topic_uri_invalid_when_uri_has_empty_use_name_local(self): + uri = "/" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_topic_uri_invalid_when_uri_is_remote_no_authority(self): + uri = "//" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_topic_uri_invalid_when_uri_is_remote_no_authority_with_use(self): + uri = "///body.access/1/door.front_left#Door" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_topic_uri_invalid_when_uri_is_missing_use_remote(self): + uri = "//VCU.myvin///door.front_left#Door" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_topic_uri_invalid_when_uri_is_missing_use_name_remote(self): + uri = "/1/door.front_left#Door" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_topic_uri_invalid_when_uri_is_missing_use_name_local(self): + uri = "//VCU.myvin//1" + status = UriValidator.validate(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_with_version_when_it_is_valid_remote(self): + uri = "//bo.cloud/petapp/1/rpc.response" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_topic_uri_no_version_when_it_is_valid_remote(self): + uri = "//bo.cloud/petapp//rpc.response" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_topic_uri_with_version_when_it_is_valid_local(self): + uri = "/petapp/1/rpc.response" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_topic_uri_no_version_when_it_is_valid_local(self): + uri = "/petapp//rpc.response" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_topic_uri_invalid_when_uri_has_schema_only(self): + uri = ":" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_with_version_when_it_is_not_valid_missing_rpc_response_local(self): + uri = "/petapp/1/dog" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_with_version_when_it_is_not_valid_missing_rpc_response_remote(self): + uri = "//petapp/1/dog" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_invalid_when_uri_is_remote_no_authority(self): + uri = "//" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_invalid_when_uri_is_remote_no_authority_with_use(self): + uri = "///body.access/1" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_invalid_when_uri_is_missing_use(self): + uri = "//VCU.myvin" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_invalid_when_uri_is_missing_use_name_remote(self): + uri = "/1" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_topic_uri_invalid_when_uri_is_missing_use_name_local(self): + uri = "//VCU.myvin//1" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_with_version_when_it_is_valid_remote(self): + uri = "//VCU.myvin/body.access/1/rpc.UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_method_uri_no_version_when_it_is_valid_remote(self): + uri = "//VCU.myvin/body.access//rpc.UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_method_uri_with_version_when_it_is_valid_local(self): + uri = "/body.access/1/rpc.UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_method_uri_no_version_when_it_is_valid_local(self): + uri = "/body.access//rpc.UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_success) + + def test_rpc_method_uri_invalid_when_uri_has_schema_only(self): + uri = ":" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_with_version_when_it_is_not_valid_not_rpc_method_local(self): + uri = "/body.access//UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_with_version_when_it_is_not_valid_not_rpc_method_remote(self): + uri = "//body.access/1/UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_invalid_when_uri_is_remote_no_authority(self): + uri = "//" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_invalid_when_uri_is_remote_no_authority_with_use(self): + uri = "///body.access/1/rpc.UpdateDoor" + uuri = LongUriSerializer().deserialize(uri) + status = UriValidator.validate_rpc_method(uuri) + self.assertEqual("", str(uuri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_invalid_when_uri_is_remote_missing_authority_remotecase(self): + uuri = UUri(entity=UEntity(name="body.access"),resource=UResource(name="rpc",instance="UpdateDoor"),authority= + UAuthority()) + status = UriValidator.validate_rpc_method(uuri) + self.assertTrue(status.is_failure()) + self.assertEqual("Uri is remote missing uAuthority.", status.get_message()) + + def test_rpc_method_uri_invalid_when_uri_is_missing_use(self): + uri = "//VCU.myvin" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_invalid_when_uri_is_missing_use_name_local(self): + uri = "/1/rpc.UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_rpc_method_uri_invalid_when_uri_is_missing_use_name_remote(self): + uri = "//VCU.myvin//1/rpc.UpdateDoor" + status = UriValidator.validate_rpc_method(LongUriSerializer().deserialize(uri)) + self.assertTrue(status.is_failure()) + + def test_all_valid_uris(self): + # Access the "validUris" array + valid_uris = self.get_json_object()["validUris"] + for valid_uri in valid_uris: + uuri = LongUriSerializer().deserialize(valid_uri) + status = UriValidator.validate(uuri) + self.assertTrue(status.is_success()) + + def test_all_invalid_uris(self): + # Access the "invalidUris" array + invalid_uris = self.get_json_object()["invalidUris"] + for invalid_uri in invalid_uris: + uuri = LongUriSerializer().deserialize(invalid_uri["uri"]) + status = UriValidator.validate(uuri) + self.assertTrue(status.is_failure()) + self.assertEqual(status.get_message(), invalid_uri["status_message"]) + + def test_all_valid_rpc_uris(self): + valid_rpc_uris = self.get_json_object()["validRpcUris"] + for valid_rpc_uri in valid_rpc_uris: + uuri = LongUriSerializer().deserialize(valid_rpc_uri) + status = UriValidator.validate_rpc_method(uuri) + self.assertTrue(status.is_success) + + def test_all_invalid_rpc_uris(self): + invalid_rpc_uris = self.get_json_object()["invalidRpcUris"] + for invalid_rpc_uri in invalid_rpc_uris: + uuri = LongUriSerializer().deserialize(invalid_rpc_uri["uri"]) + status = UriValidator.validate_rpc_method(uuri) + self.assertTrue(status.is_failure()) + self.assertEqual(status.get_message(), invalid_rpc_uri["status_message"]) + + def test_all_valid_rpc_response_uris(self): + valid_rpc_response_uris = self.get_json_object()["validRpcResponseUris"] + for valid_rpc_response_uri in valid_rpc_response_uris: + uuri = LongUriSerializer().deserialize(valid_rpc_response_uri) + status = UriValidator.validate_rpc_response(uuri) + self.assertTrue(UriValidator.is_rpc_response(uuri)) + self.assertTrue(status.is_success) + + def test_valid_rpc_response_uri(self): + uuri = UUri(entity=UEntity(name="neelam"),resource=UResource(name="rpc",id=19999)) + status = UriValidator.validate_rpc_response(uuri) + self.assertTrue(UriValidator.is_rpc_response(uuri)) + self.assertTrue(status.is_success) + + def test_all_invalid_rpc_response_uris(self): + invalid_rpc_response_uris = self.get_json_object()["invalidRpcResponseUris"] + for invalid_rpc_response_uri in invalid_rpc_response_uris: + uuri = LongUriSerializer().deserialize(invalid_rpc_response_uri) + status = UriValidator.validate_rpc_response(uuri) + self.assertTrue(status.is_failure()) + + @staticmethod + def get_json_object(): + current_directory = os.getcwd() + json_file_path = os.path.join(current_directory,"tests","test_uri","test_validator", "uris.json") + + with open(json_file_path, 'r') as json_file: + json_data = json.load(json_file) + + return json_data + + +if __name__ == '__main__': + unittest.main() diff --git a/tests/test_uri/test_validator/uris.json b/tests/test_uri/test_validator/uris.json new file mode 100644 index 0000000..2865428 --- /dev/null +++ b/tests/test_uri/test_validator/uris.json @@ -0,0 +1,107 @@ +{ + "validUris": [ + "/hartley", + "/hartley//", + "hartley/0", + "/1", + "/body.access/1", + "/body.access/1/door.front_left#Door", + "//vcu.vin/body.access/1/door.front_left#Door", + "/body.access/1/rpc.OpenWindow", + "/body.access/1/rpc.response" + ], + "invalidUris": [ + { + "uri": "/", + "status_message": "Uri is empty.", + "reason": "Empty Uri" + }, + { + "uri": "//", + "status_message": "Uri is empty.", + "reason": "Empty Uri" + }, + { + "uri": "//vcu", + "status_message": "Uri is missing uSoftware Entity name.", + "reason": "Missing entity name." + }, + { + "uri": "//vcu.vin/", + "status_message": "Uri is missing uSoftware Entity name.", + "reason": "Missing entity name." + }, + { + "uri": "", + "status_message": "Uri is empty.", + "reason": "Empty Uri" + }, + { + "uri": ":", + "status_message": "Uri is empty.", + "reason": "Contains only schema" + }, + { + "uri": "///", + "status_message": "Uri is empty.", + "reason": "Empty Authority" + }, + { + "uri": "////", + "status_message": "Uri is empty.", + "reason": "Empty Uri" + }, + { + "uri": "1", + "status_message": "Uri is empty.", + "reason": "Invalid Uri, must begin with \"\/\"" + }, + { + "uri": "a", + "status_message": "Uri is empty.", + "reason": "Invalid Uri, must begin with \"\/\"" + } + ], + "validRpcUris": [ + "/petapp/1/rpc.OpenWindow", + "/petapp/1/rpc.response" + ], + "validRpcResponseUris": [ + "/petapp/1/rpc.response" + ], + "invalidRpcResponseUris": [ + "/petapp/1/rpc.OpenWindow" + ], + "invalidRpcUris": [ + { + "uri": "/petapp//", + "reason": "Missing uE version", + "status_message": "Invalid RPC method uri. Uri should be the method to be called, or method from response." + }, + { + "uri": "/petapp", + "reason": "Missing uE version", + "status_message": "Invalid RPC method uri. Uri should be the method to be called, or method from response." + }, + { + "uri": "/petapp/1/", + "reason": "Missing RPC Method Name", + "status_message": "Invalid RPC method uri. Uri should be the method to be called, or method from response." + }, + { + "uri": "/petapp/1/rpc", + "reason": "Missing RPC Method Name", + "status_message": "Invalid RPC method uri. Uri should be the method to be called, or method from response." + }, + { + "uri": "/petapp/1/dummy", + "reason": "Missing RPC Method Name", + "status_message": "Invalid RPC method uri. Uri should be the method to be called, or method from response." + }, + { + "uri": "/petapp/1/rpc_dummy", + "reason": "Missing RPC Method Name", + "status_message": "Invalid RPC method uri. Uri should be the method to be called, or method from response." + } + ] +} \ No newline at end of file diff --git a/tests/test_uuid/__init__.py b/tests/test_uuid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_uuid/test_factory/__init__.py b/tests/test_uuid/test_factory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_uuid/test_factory/test_uuidfactory.py b/tests/test_uuid/test_factory/test_uuidfactory.py new file mode 100644 index 0000000..badf2f4 --- /dev/null +++ b/tests/test_uuid/test_factory/test_uuidfactory.py @@ -0,0 +1,284 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +from datetime import datetime, timedelta +import unittest +from uprotocol.uuid.serializer.longuuidserializer import LongUuidSerializer +from uprotocol.uuid.serializer.microuuidserializer import MicroUuidSerializer +from uprotocol.uuid.factory.uuidfactory import UUIDFactory, Factories +from uprotocol.uuid.factory.uuidutils import UUIDUtils, Version +from uprotocol.proto.uuid_pb2 import UUID + + + +class TestUUIDFactory(unittest.TestCase): + + def test_uuidv8_creation(self): + now = datetime.now() + uuid = Factories.UPROTOCOL.create(now) + version = UUIDUtils.getVersion(uuid) + time = UUIDUtils.getTime(uuid) + bytes_data = MicroUuidSerializer.instance().serialize(uuid) + uuid_string = LongUuidSerializer.instance().serialize(uuid) + + self.assertIsNotNone(uuid) + self.assertTrue(UUIDUtils.isUProtocol(uuid)) + self.assertTrue(UUIDUtils.isuuid(uuid)) + self.assertFalse(UUIDUtils.isUuidv6(uuid)) + self.assertTrue(version) + self.assertTrue(time) + self.assertEqual(time, int(now.timestamp() * 1000)) + + self.assertGreater(len(bytes_data), 0) + self.assertFalse(uuid_string.isspace()) + + uuid1 = MicroUuidSerializer.instance().deserialize(bytes_data) + uuid2 = LongUuidSerializer.instance().deserialize(uuid_string) + + self.assertNotEqual(uuid1, UUID()) + self.assertNotEqual(uuid2, UUID()) + self.assertEqual(uuid, uuid1) + self.assertEqual(uuid, uuid2) + + def test_uuidv8_creation_with_null_instant(self): + uuid = Factories.UPROTOCOL.create(None) + version = UUIDUtils.getVersion(uuid) + time = UUIDUtils.getTime(uuid) + bytes_data = MicroUuidSerializer.instance().serialize(uuid) + uuid_string = LongUuidSerializer.instance().serialize(uuid) + + self.assertIsNotNone(uuid) + self.assertTrue(UUIDUtils.isUProtocol(uuid)) + self.assertTrue(UUIDUtils.isuuid(uuid)) + self.assertFalse(UUIDUtils.isUuidv6(uuid)) + self.assertTrue(version) + self.assertTrue(time) + self.assertGreater(len(bytes_data), 0) + self.assertFalse(uuid_string.isspace()) + + uuid1 = MicroUuidSerializer.instance().deserialize(bytes_data) + uuid2 = LongUuidSerializer.instance().deserialize(uuid_string) + + self.assertNotEqual(uuid1, UUID()) + self.assertNotEqual(uuid2, UUID()) + self.assertEqual(uuid, uuid1) + self.assertEqual(uuid, uuid2) + + def test_uuidv8_overflow(self): + uuid_list = [] + max_count = 4095 + + now = datetime.now() + for i in range(max_count * 2): + uuid_list.append(Factories.UPROTOCOL.create(now)) + + self.assertEqual(UUIDUtils.getTime(uuid_list[0]), UUIDUtils.getTime(uuid_list[i])) + self.assertEqual(uuid_list[0].lsb, uuid_list[i].lsb) + if i > max_count: + self.assertEqual(uuid_list[max_count].msb, uuid_list[i].msb) + + def test_uuidv6_creation_with_instant(self): + now = datetime.now() + uuid = Factories.UUIDV6.create(now) + version = UUIDUtils.getVersion(uuid) + # time = UUIDUtils.getTime(uuid) + bytes_data = MicroUuidSerializer.instance().serialize(uuid) + uuid_string = LongUuidSerializer.instance().serialize(uuid) + + self.assertIsNotNone(uuid) + self.assertTrue(UUIDUtils.isUuidv6(uuid)) + self.assertTrue(UUIDUtils.isuuid(uuid)) + self.assertFalse(UUIDUtils.isUProtocol(uuid)) + self.assertTrue(version) + # self.assertTrue(time) + # self.assertEqual(time, int(17007094616498160 * 1000)) + self.assertGreater(len(bytes_data), 0) + self.assertFalse(uuid_string.isspace()) + + uuid1 = MicroUuidSerializer.instance().deserialize(bytes_data) + uuid2 = LongUuidSerializer.instance().deserialize(uuid_string) + + self.assertNotEqual(uuid1, UUID()) + self.assertNotEqual(uuid2, UUID()) + self.assertEqual(uuid, uuid1) + self.assertEqual(uuid, uuid2) + + def test_uuidv6_creation_with_null_instant(self): + uuid = Factories.UUIDV6.create(None) + version = UUIDUtils.getVersion(uuid) + time = UUIDUtils.getTime(uuid) + bytes_data = MicroUuidSerializer.instance().serialize(uuid) + uuid_string = LongUuidSerializer.instance().serialize(uuid) + + self.assertIsNotNone(uuid) + self.assertTrue(UUIDUtils.isUuidv6(uuid)) + self.assertFalse(UUIDUtils.isUProtocol(uuid)) + self.assertTrue(UUIDUtils.isuuid(uuid)) + self.assertTrue(version) + self.assertTrue(time) + self.assertGreater(len(bytes_data), 0) + self.assertFalse(uuid_string.isspace()) + + uuid1 = MicroUuidSerializer.instance().deserialize(bytes_data) + uuid2 = LongUuidSerializer.instance().deserialize(uuid_string) + + self.assertNotEqual(uuid1, UUID()) + self.assertNotEqual(uuid2, UUID()) + self.assertEqual(uuid, uuid1) + self.assertEqual(uuid, uuid2) + + def test_UUIDUtils_for_random_uuid(self): + uuid = LongUuidSerializer.instance().deserialize("195f9bd1-526d-4c28-91b1-ff34c8e3632d") + version = UUIDUtils.getVersion(uuid) + time = UUIDUtils.getTime(uuid) + bytes_data = MicroUuidSerializer.instance().serialize(uuid) + uuid_string = LongUuidSerializer.instance().serialize(uuid) + + self.assertIsNotNone(uuid) + self.assertFalse(UUIDUtils.isUuidv6(uuid)) + self.assertFalse(UUIDUtils.isUProtocol(uuid)) + self.assertFalse(UUIDUtils.isuuid(uuid)) + self.assertTrue(version) + self.assertFalse(time) + self.assertGreater(len(bytes_data), 0) + self.assertFalse(uuid_string.isspace()) + + uuid1 = MicroUuidSerializer.instance().deserialize(bytes_data) + uuid2 = LongUuidSerializer.instance().deserialize(uuid_string) + + self.assertNotEqual(uuid1, UUID()) + self.assertNotEqual(uuid2, UUID()) + self.assertEqual(uuid, uuid1) + self.assertEqual(uuid, uuid2) + + def test_UUIDUtils_for_empty_uuid(self): + uuid = UUID() + version = UUIDUtils.getVersion(uuid) + time = UUIDUtils.getTime(uuid) + bytes_data = MicroUuidSerializer.instance().serialize(uuid) + uuid_string = LongUuidSerializer.instance().serialize(uuid) + + self.assertIsNotNone(uuid) + self.assertFalse(UUIDUtils.isUuidv6(uuid)) + self.assertFalse(UUIDUtils.isUProtocol(uuid)) + self.assertTrue(version) + self.assertEqual(version, Version.VERSION_UNKNOWN) + self.assertFalse(time) + self.assertGreater(len(bytes_data), 0) + self.assertFalse(uuid_string.isspace()) + self.assertFalse(UUIDUtils.isUuidv6(None)) + self.assertFalse(UUIDUtils.isUProtocol(None)) + self.assertFalse(UUIDUtils.isuuid(None)) + + uuid1 = MicroUuidSerializer.instance().deserialize(bytes_data) + + self.assertTrue(uuid1, UUID()) + self.assertEqual(uuid, uuid1) + + uuid2 = LongUuidSerializer.instance().deserialize(uuid_string) + self.assertTrue(uuid2, UUID()) + self.assertEqual(uuid, uuid2) + + def test_UUIDUtils_for_null_uuid(self): + self.assertFalse(UUIDUtils.getVersion(None)) + self.assertEqual(len(MicroUuidSerializer.instance().serialize(None)), 0) + self.assertEqual(len(LongUuidSerializer.instance().serialize(None)),0) + self.assertFalse(UUIDUtils.isUuidv6(None)) + self.assertFalse(UUIDUtils.isUProtocol(None)) + self.assertFalse(UUIDUtils.isuuid(None)) + self.assertFalse(UUIDUtils.getTime(None)) + + def test_uuidutils_from_invalid_uuid(self): + uuid = UUID(msb=9 << 12, lsb=0) # Invalid UUID type + self.assertFalse(UUIDUtils.getVersion(uuid)) + self.assertFalse(UUIDUtils.getTime(uuid)) + self.assertTrue(len(MicroUuidSerializer.instance().serialize(uuid)) > 0) + self.assertFalse(LongUuidSerializer.instance().serialize(uuid).isspace()) + self.assertFalse(UUIDUtils.isUuidv6(uuid)) + self.assertFalse(UUIDUtils.isUProtocol(uuid)) + self.assertFalse(UUIDUtils.isuuid(uuid)) + self.assertFalse(UUIDUtils.getTime(uuid)) + + def test_uuidutils_fromstring_with_invalid_string(self): + uuid = LongUuidSerializer.instance().deserialize(None) + self.assertTrue(uuid == UUID()) + uuid1 = LongUuidSerializer.instance().deserialize("") + self.assertTrue(uuid1 == UUID()) + + def test_uuidutils_frombytes_with_invalid_bytes(self): + uuid = MicroUuidSerializer.instance().deserialize(None) + self.assertTrue(uuid == UUID()) + uuid1 = MicroUuidSerializer.instance().deserialize(bytearray()) + self.assertTrue(uuid1 == UUID()) + + def test_create_uprotocol_uuid_in_the_past(self): + past = datetime.utcnow() - timedelta(seconds=10) + uuid = Factories.UPROTOCOL.create(past) + time = UUIDUtils.getTime(uuid) + self.assertTrue(UUIDUtils.isUProtocol(uuid)) + self.assertTrue(UUIDUtils.isuuid(uuid)) + self.assertTrue(time is not None) + self.assertEqual(time, int(past.timestamp() * 1000)) + + def test_create_uprotocol_uuid_with_different_time_values(self): + uuid = Factories.UPROTOCOL.create() + import time + time.sleep(0.01) # Sleep for 10 milliseconds + uuid1 = Factories.UPROTOCOL.create() + time = UUIDUtils.getTime(uuid) + time1 = UUIDUtils.getTime(uuid1) + + self.assertTrue(UUIDUtils.isUProtocol(uuid)) + self.assertTrue(UUIDUtils.isuuid(uuid)) + self.assertTrue(UUIDUtils.isUProtocol(uuid1)) + self.assertTrue(UUIDUtils.isuuid(uuid1)) + + self.assertTrue(time is not None) + self.assertTrue(time1 is not None) + self.assertNotEqual(time, time1) + + def test_create_both_uuidv6_and_v8_to_compare_performance(self): + uuidv6_list = [] + uuidv8_list = [] + max_count = 10000 + + start = datetime.utcnow() + for _ in range(max_count): + uuidv8_list.append(Factories.UPROTOCOL.create()) + v8_diff = datetime.utcnow() - start + + start = datetime.utcnow() + for _ in range(max_count): + uuidv6_list.append(Factories.UUIDV6.create()) + v6_diff = datetime.utcnow() - start + # print( + # f"UUIDv8: [{v8_diff.total_seconds() / max_count}s] UUIDv6: [{v6_diff.total_seconds() / max_count}s]") + + +if __name__ == '__main__': + unittest.main() + diff --git a/tests/test_uuid/test_validator/__init__.py b/tests/test_uuid/test_validator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_uuid/test_validator/test_uuidvalidator.py b/tests/test_uuid/test_validator/test_uuidvalidator.py new file mode 100644 index 0000000..45e0455 --- /dev/null +++ b/tests/test_uuid/test_validator/test_uuidvalidator.py @@ -0,0 +1,139 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +import unittest +from datetime import datetime, timezone + +from uprotocol.uuid.factory.uuidutils import UUIDUtils +from uprotocol.uuid.serializer.longuuidserializer import LongUuidSerializer + +from uprotocol.validation.validationresult import ValidationResult +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.proto.ustatus_pb2 import UCode +from uprotocol.uuid.factory.uuidfactory import Factories +from uprotocol.uuid.validate.uuidvalidator import UuidValidator, Validators + + +class TestUuidValidator(unittest.TestCase): + def test_validator_with_good_uuid(self): + uuid = Factories.UPROTOCOL.create() + status = UuidValidator.get_validator(uuid).validate(uuid) + self.assertEqual(ValidationResult.STATUS_SUCCESS, status) + + def test_good_uuid_string(self): + status = Validators.UPROTOCOL.validator().validate( + Factories.UPROTOCOL.create() + ) + self.assertEqual(status, ValidationResult.STATUS_SUCCESS) + # self.assertTrue(ValidationResult.success().__eq__(status)) + + def test_invalid_uuid(self): + uuid = UUID(msb=0, lsb=0) + status = UuidValidator.get_validator(uuid).validate(uuid) + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual( + "Invalid UUID Version,Invalid UUID Variant,Invalid UUID Time", + status.message, + ) + + def test_invalid_time_uuid(self): + epoch_time = datetime.utcfromtimestamp(0).replace(tzinfo=timezone.utc) + + uuid = Factories.UPROTOCOL.create( + epoch_time + ) + status = Validators.UPROTOCOL.validator().validate(uuid) + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid UUID Time", status.message) + + def test_uuidv8_with_invalid_uuids(self): + validator = Validators.UPROTOCOL.validator() + self.assertIsNotNone(validator) + status = validator.validate(None) + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid UUIDv8 Version,Invalid UUID Time", status.message) + + def test_uuidv8_with_invalid_types(self): + uuidv6 = Factories.UUIDV6.create() + uuid = UUID(msb=0, lsb=0) + uuidv4 = LongUuidSerializer.instance().deserialize("195f9bd1-526d-4c28-91b1-ff34c8e3632d") + + validator = Validators.UPROTOCOL.validator() + self.assertIsNotNone(validator) + + status = validator.validate(uuidv6) + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + self.assertEqual("Invalid UUIDv8 Version", status.message) + + status1 = validator.validate(uuid) + self.assertEqual(UCode.INVALID_ARGUMENT, status1.code) + self.assertEqual("Invalid UUIDv8 Version,Invalid UUID Time", status1.message) + + status2 = validator.validate(uuidv4) + self.assertEqual(UCode.INVALID_ARGUMENT, status2.code) + self.assertEqual("Invalid UUIDv8 Version,Invalid UUID Time", status2.message) + + def test_good_uuidv6(self): + uuid = Factories.UUIDV6.create() + + validator = UuidValidator.get_validator(uuid) + self.assertIsNotNone(validator) + self.assertTrue(UUIDUtils.isUuidv6(uuid)) + self.assertEqual(UCode.OK, validator.validate(uuid).code) + + def test_uuidv6_with_invalid_uuid(self): + uuid = UUID(msb=9 << 12, lsb=0) + validator = Validators.UUIDV6.validator() + self.assertIsNotNone(validator) + status = validator.validate(uuid) + self.assertEqual( + "Not a UUIDv6 Version,Invalid UUIDv6 variant,Invalid UUID Time", + status.message, + ) + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + + def test_uuidv6_with_null_uuid(self): + validator = Validators.UUIDV6.validator() + self.assertIsNotNone(validator) + status = validator.validate(None) + self.assertEqual( + "Not a UUIDv6 Version,Invalid UUIDv6 variant,Invalid UUID Time", + status.message, + ) + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + + def test_uuidv6_with_uuidv8(self): + uuid = Factories.UPROTOCOL.create() + validator = Validators.UUIDV6.validator() + self.assertIsNotNone(validator) + status = validator.validate(uuid) + self.assertEqual("Not a UUIDv6 Version", status.message) + self.assertEqual(UCode.INVALID_ARGUMENT, status.code) + + +if __name__ == "__main__": + unittest.main() diff --git a/uprotocol/__init__.py b/uprotocol/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/cloudevent/README.adoc b/uprotocol/cloudevent/README.adoc new file mode 100644 index 0000000..28b512e --- /dev/null +++ b/uprotocol/cloudevent/README.adoc @@ -0,0 +1,67 @@ += uProtocol CloudEvents +:toc: +:sectnums: + + +== Overview + +https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/up-l1/cloudevents.adoc[uProtocol CloudEvents] is a common message envelope that could be used to carry way to represent uProtocol transport layer information `UUri` (source), `UPayload`, and `UAttributes`. `CloudEvents` are used by a number of Device-2-Cloud and Cloud-2-Device based transports such as MQTT and HTTP, however it could also be used by any transport (ex. Binder). + + +=== CloudEventFactory +Factory class that builds the various types of CloudEvents for uProtocol (publish, notification, request, response) + +== Examples + +=== Building an uuri +[source,python] +---- + uri = UUri(entity=UEntity(name="body.access"), + resource=UResource(name="door", instance="front_left", message="Door")) + source= LongUriSerializer().serialize(uri) +---- + +=== Build proto payload +[source,python] + +---- +ce_proto = CloudEvent(spec_version="1.0", source="https://example.com", id="hello", type="example.demo", + proto_data=any_pb2.Any()) + +any_obj = any_pb2.Any() +any_obj.Pack(ce_proto) +proto_payload = any_obj + +---- + +=== Build UCloudEvent Attributes +[source,python] + +---- +u_cloud_event_attributes = UCloudEventAttributesBuilder().with_hash("somehash").with_priority( +UPriority.UPRIORITY_CS1).with_ttl(3).build() + +---- + +=== Build publish cloud event +[source,python] + +---- +cloud_event = CloudEventFactory.publish(source, proto_payload, u_cloud_event_attributes) +# test all attributes +assertEqual("1.0", UCloudEvent.get_specversion(cloud_event)) +assertIsNotNone(UCloudEvent.get_id(cloud_event)) +assertEqual(source, UCloudEvent.get_source(cloud_event)) +assertEqual(UCloudEvent.get_event_type(UMessageType.UMESSAGE_TYPE_PUBLISH), + UCloudEvent.get_type(cloud_event)) +assertNotIn("sink", cloud_event.get_attributes()) +assertEqual("somehash", UCloudEvent.get_hash(cloud_event)) +assertEqual(UPriority.Name(UPriority.UPRIORITY_CS1), UCloudEvent.get_priority(cloud_event)) +assertEqual(3, UCloudEvent.get_ttl(cloud_event)) +assertEqual(proto_payload.SerializeToString(), cloud_event.get_data()) + +---- + + + + diff --git a/uprotocol/cloudevent/__init__.py b/uprotocol/cloudevent/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/cloudevent/datamodel/__init__.py b/uprotocol/cloudevent/datamodel/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/cloudevent/datamodel/ucloudeventattributes.py b/uprotocol/cloudevent/datamodel/ucloudeventattributes.py new file mode 100644 index 0000000..d30f3e3 --- /dev/null +++ b/uprotocol/cloudevent/datamodel/ucloudeventattributes.py @@ -0,0 +1,176 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + +from typing import Optional + +from uprotocol.proto.uattributes_pb2 import UPriority +from uprotocol.proto.uattributes_pb2 import UPriority + + +class UCloudEventAttributes: + """ + Specifies the properties that can configure the UCloudEvent. + """ + + def __init__(self, priority: UPriority, hash_value: str = None, ttl: int = None, token: str = None): + """ + Construct the properties object.

+ @param hash_value: an HMAC generated on the data portion of the CloudEvent message using the device key. + @param priority: uProtocol Prioritization classifications. + @param ttl: How long this event should live for after it was generated (in milliseconds). Events without this + attribute (or value is 0) MUST NOT timeout. + @param token: Oauth2 access token to perform the access request defined in the request message. + """ + self.hash = hash_value + self.priority = priority + self.ttl = ttl + self.token = token + + @staticmethod + def empty(): + """ + Static factory method for creating an empty cloud event attributes object, to avoid working with null

+ @return: Returns an empty cloud event attributes that indicates that there are no added additional + attributes to configure. + """ + return UCloudEventAttributes(None, None, None, None) + + def is_empty(self): + """ + Indicates that there are no added additional attributes to configure when building a CloudEvent.

+ @return: Returns true if this attributes container is an empty container and has no valuable information in + building a CloudEvent. + """ + return (self.hash is None or self.hash.isspace()) and (self.ttl is None) and ( + self.token is None or self.token.isspace()) and (self.priority is None or self.priority.isspace()) + + def get_hash(self) -> str: + """ + An HMAC generated on the data portion of the CloudEvent message using the device key.

+ @return: Returns an Optional hash attribute. + """ + return self.hash if self.hash and self.hash.strip() else None + + def get_priority(self) -> UPriority: + """ + uProtocol Prioritization classifications.

+ @return: Returns an Optional priority attribute. + """ + return self.priority + + def get_ttl(self) -> int: + """ + How long this event should live for after it was generated (in milliseconds).

+ @return: Returns an Optional time to live attribute. + """ + return self.ttl + + def get_token(self) -> str: + """ + Oauth2 access token to perform the access request defined in the request message.

+ @return: Returns an Optional OAuth token attribute. + """ + return self.token if self.token and self.token.strip() else None + + def __eq__(self, other): + if self is other: + return True + if not isinstance(other, UCloudEventAttributes): + return False + return ( + self.hash == other.hash and self.priority == other.priority and self.ttl == other.ttl and self.token + == other.token) + + def __hash__(self): + return hash((self.hash, self.priority, self.ttl, self.token)) + + def __str__(self): + return f"UCloudEventAttributes{{hash='{self.hash}', priority={self.priority}," \ + f" ttl={self.ttl}, token='{self.token}'}}" + + +class UCloudEventAttributesBuilder: + """ + Builder for constructing the UCloudEventAttributes. + """ + + def __init__(self): + self.hash = None + self.priority = None + self.ttl = None + self.token = None + + def with_hash(self, hash_value: str): + """ + Add an HMAC generated on the data portion of the CloudEvent message using the device key.

+ @param hash_value: hash an HMAC generated on the data portion of the CloudEvent message using the device key. + @return: Returns the UCloudEventAttributesBuilder with the configured hash. + """ + self.hash = hash_value + return self + + def with_priority(self, priority: UPriority): + """ + Add a uProtocol Prioritization classifications.

+ @param priority: priority uProtocol Prioritization classifications. + @return: Returns the UCloudEventAttributesBuilder with the configured priority. + """ + self.priority = UPriority.Name(priority) + return self + + def with_ttl(self, ttl: int): + """ + Add a time to live which is how long this event should live for after it was generated (in milliseconds). + Events without this attribute (or value is 0) MUST NOT timeout.

+ @param ttl: How long this event should live for after it was generated (in milliseconds). Events without this + attribute (or value is 0) MUST NOT timeout. + @return: Returns the UCloudEventAttributesBuilder with the configured time to live. + """ + self.ttl = ttl + return self + + def with_token(self, token: str): + """ + Add an Oauth2 access token to perform the access request defined in the request message.

+ @param token: An Oauth2 access token to perform the access request defined in the request message. + @return: Returns the UCloudEventAttributesBuilder with the configured OAuth token. + """ + self.token = token + return self + + def build(self): + """ + Construct the UCloudEventAttributes from the builder.

+ @return: Returns a constructed UProperty. + """ + return UCloudEventAttributes(self.priority, self.hash, self.ttl, self.token) + + +if __name__ == "__main__": + # Example usage: + attributes = UCloudEventAttributesBuilder().with_hash("abc123").with_priority(UPriority.UPRIORITY_CS0).with_ttl( + 1000).with_token("xyz456").build() + print(attributes) diff --git a/uprotocol/cloudevent/factory/__init__.py b/uprotocol/cloudevent/factory/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/cloudevent/factory/cloudeventfactory.py b/uprotocol/cloudevent/factory/cloudeventfactory.py new file mode 100644 index 0000000..c80ed74 --- /dev/null +++ b/uprotocol/cloudevent/factory/cloudeventfactory.py @@ -0,0 +1,207 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from cloudevents.http import CloudEvent +from google.protobuf import empty_pb2 +from google.protobuf.any_pb2 import Any + +from uprotocol.cloudevent.datamodel.ucloudeventattributes import UCloudEventAttributes +from uprotocol.cloudevent.factory.ucloudevent import UCloudEvent +from uprotocol.proto.uattributes_pb2 import UMessageType +from uprotocol.uuid.factory.uuidfactory import Factories +from uprotocol.uuid.serializer.longuuidserializer import LongUuidSerializer + + +# A factory is a part of the software has methods to generate concrete objects, usually of the same type or +# interface. CloudEvents is a specification for describing events in a common way. We will use CloudEvents to +# formulate all kinds of events (messages) that will be sent to and from devices. The CloudEvent factory knows how +# to generate CloudEvents of the 4 core types: req.v1, res.v1 and pub.v1 +class CloudEventFactory: + PROTOBUF_CONTENT_TYPE = "application/x-protobuf" + + @staticmethod + def request(application_uri_for_rpc: str, service_method_uri: str, request_id: str, proto_payload: Any, + attributes: UCloudEventAttributes) -> CloudEvent: + """ + Create a CloudEvent for an event for the use case of: RPC Request message. + @param application_uri_for_rpc: The uri for the application requesting the RPC. + @param service_method_uri: The uri for the method to be called on the service Ex. :/body.access/1/rpc.UpdateDoor + @param request_id:The attribute id from the original request + @param proto_payload:Protobuf Any object with the Message command to be executed on the sink service. + @param attributes: Additional attributes such as ttl, hash, priority and token. + @return: Returns an request CloudEvent. + """ + event_id = CloudEventFactory.generate_cloud_event_id() + cloud_event = CloudEventFactory.build_base_cloud_event(event_id, application_uri_for_rpc, + + proto_payload.SerializeToString(), + proto_payload.DESCRIPTOR.full_name, attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_REQUEST) + ) + cloud_event.__setitem__("sink", service_method_uri) + cloud_event.__setitem__("reqid", request_id) + + return cloud_event + + @staticmethod + def response(application_uri_for_rpc: str, service_method_uri: str, request_id: str, proto_payload: Any, + attributes: UCloudEventAttributes) -> CloudEvent: + """ + Create a CloudEvent for an event for the use case of: RPC Response message. + @param application_uri_for_rpc: The destination of the response. The uri for the original application that + requested the RPC and this response is for. + @param service_method_uri: The uri for the method that was called on the service Ex. + :/body.access/1/rpc.UpdateDoor + @param request_id:The cloud event id from the original request cloud event that this response if for. + @param proto_payload: The protobuf serialized response message as defined by the application interface or the + UStatus message containing the details of an error. + @param attributes: Additional attributes such as ttl, hash and priority. + @return: Returns an response CloudEvent. + """ + event_id = CloudEventFactory.generate_cloud_event_id() + cloud_event = CloudEventFactory.build_base_cloud_event(event_id, service_method_uri, + + proto_payload.SerializeToString(), + proto_payload.DESCRIPTOR.full_name, attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_RESPONSE)) + cloud_event.__setitem__("sink", application_uri_for_rpc) + cloud_event.__setitem__("reqid", request_id) + + return cloud_event + + @staticmethod + def failed_response(application_uri_for_rpc: str, service_method_uri: str, request_id: str, + communication_status: int, attributes: UCloudEventAttributes) -> CloudEvent: + """ + Create a CloudEvent for an event for the use case of: RPC Response message that failed. + @param application_uri_for_rpc: The destination of the response. The uri for the original application that + requested the RPC and this response is for. + @param service_method_uri: The uri for the method that was called on the service Ex. + :/body.access/1/rpc.UpdateDoor + @param request_id:The cloud event id from the original request cloud event that this response if for. + @param communication_status: A {@link Code} value that indicates of a platform communication error while + delivering this CloudEvent. + @param attributes:Additional attributes such as ttl, hash and priority. + @return:Returns an response CloudEvent Response for the use case of RPC Response message that failed. + """ + event_id = CloudEventFactory.generate_cloud_event_id() + # Create an Any message packing an Empty message + empty_proto_payload = Any() + empty_proto_payload.Pack(empty_pb2.Empty()) + cloud_event = CloudEventFactory.build_base_cloud_event(event_id, service_method_uri, + + empty_proto_payload.SerializeToString(), # Empty payload + "google.protobuf.Empty", attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_RESPONSE) + ) + cloud_event.__setitem__("sink", application_uri_for_rpc) + cloud_event.__setitem__("reqid", request_id) + cloud_event.__setitem__("commstatus", communication_status) + + return cloud_event + + @staticmethod + def publish(source: str, proto_payload: Any, attributes: UCloudEventAttributes) -> CloudEvent: + """ + Create a CloudEvent for an event for the use case of: Publish generic message. + @param source:The uri of the topic being published. + @param proto_payload:protobuf Any object with the Message to be published. + @param attributes:Additional attributes such as ttl, hash and priority. + @return:Returns a publish CloudEvent. + """ + event_id = CloudEventFactory.generate_cloud_event_id() + cloud_event = CloudEventFactory.build_base_cloud_event(event_id, source, proto_payload.SerializeToString(), + proto_payload.DESCRIPTOR.full_name, attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + + return cloud_event + + @staticmethod + def notification(source: str, sink: str, proto_payload: Any, attributes: UCloudEventAttributes) -> CloudEvent: + """ + Create a CloudEvent for an event for the use case of: Publish a notification message. A published event + containing the sink (destination) is often referred to as a notification, it is an event sent to a specific + consumer. + @param source: The uri of the topic being published. + @param sink: The uri of the destination of this notification. + @param proto_payload: protobuf Any object with the Message to be published. + @param attributes: Additional attributes such as ttl, hash and priority. + @return: Returns a publish CloudEvent. + """ + event_id = CloudEventFactory.generate_cloud_event_id() + cloud_event = CloudEventFactory.build_base_cloud_event(event_id, source, proto_payload.SerializeToString(), + proto_payload.DESCRIPTOR.full_name, attributes, + UCloudEvent.get_event_type( + UMessageType.UMESSAGE_TYPE_PUBLISH)) + cloud_event.__setitem__("sink", sink) + + return cloud_event + + @staticmethod + def generate_cloud_event_id() -> str: + """ + Generate a UUIDv8 + @return: Returns a UUIDv8 id. + """ + uuid_inst = Factories.UPROTOCOL.create() + return LongUuidSerializer.instance().serialize(uuid_inst) + + @staticmethod + def build_base_cloud_event(id: str, source: str, proto_payload_bytes: bytes, proto_payload_schema: str, + attributes: UCloudEventAttributes, type) -> CloudEvent: + """ + Base CloudEvent builder that is the same for all CloudEvent types. + + @param id:Event unique identifier. + @param source: Identifies who is sending this event in the format of a uProtocol URI that can be built from a + {@link UUri} object. + @param proto_payload_bytes:The serialized Event data with the content type of "application/x-protobuf". + @param proto_payload_schema:The schema of the proto payload bytes, for example you can use + protoPayload.getTypeUrl() on your service/app object. + @param attributes:Additional cloud event attributes that can be passed in. All attributes are optional and + will be added only if they were configured. + @param type: Type of the cloud event + @return:Returns a CloudEventBuilder that can be additionally configured and then by calling .build() + construct a CloudEvent ready to be serialized and sent to the transport layer. + """ + json_attributes = {"id": id, "source": source, "type": type} + if attributes.get_hash() is not None: + json_attributes['hash'] = attributes.get_hash() + if attributes.get_ttl() is not None: + json_attributes['ttl'] = attributes.get_ttl() + if attributes.get_priority() is not None: + json_attributes['priority'] = attributes.get_priority() + if attributes.get_token() is not None: + json_attributes['token'] = attributes.get_token() + + cloud_event = CloudEvent(json_attributes, proto_payload_bytes) + + return cloud_event diff --git a/uprotocol/cloudevent/factory/ucloudevent.py b/uprotocol/cloudevent/factory/ucloudevent.py new file mode 100644 index 0000000..c85ff02 --- /dev/null +++ b/uprotocol/cloudevent/factory/ucloudevent.py @@ -0,0 +1,357 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from datetime import datetime, timedelta + +from cloudevents.http import CloudEvent +from google.protobuf import any_pb2 +from google.protobuf.message import DecodeError +from uprotocol.proto.ustatus_pb2 import UCode +from uprotocol.proto.uattributes_pb2 import UMessageType +from uprotocol.uuid.factory.uuidutils import UUIDUtils +from uprotocol.uuid.serializer.longuuidserializer import LongUuidSerializer + + +class UCloudEvent: + """ + Class to extract information from a CloudEvent. + """ + + @staticmethod + def get_source(ce: CloudEvent) -> str: + """ + Extract the source from a cloud event. The source is a mandatory attribute. The CloudEvent constructor does + not allow creating a cloud event without a source.

+ @param ce:CloudEvent with source to be extracted. + @return:Returns the String value of a CloudEvent source attribute. + """ + return UCloudEvent.extract_string_value_from_attributes("source", ce) + + @staticmethod + def get_data_content_type(ce: CloudEvent) -> str: + """ + Extract the source from a cloud event. The source is a mandatory attribute. The CloudEvent constructor does + not allow creating a cloud event without a source.

+ @param ce:CloudEvent with source to be extracted. + @return:Returns the String value of a CloudEvent source attribute. + """ + return UCloudEvent.extract_string_value_from_attributes("datacontenttype", ce) + + @staticmethod + def get_data_schema(ce: CloudEvent) -> str: + """ + Extract the source from a cloud event. The source is a mandatory attribute. The CloudEvent constructor does + not allow creating a cloud event without a source.

+ @param ce:CloudEvent with source to be extracted. + @return:Returns the String value of a CloudEvent source attribute. + """ + return UCloudEvent.extract_string_value_from_attributes("dataschema", ce) + + @staticmethod + def get_type(ce: CloudEvent) -> str: + """ + Extract the type from a cloud event. The source is a mandatory attribute. The CloudEvent constructor does + not allow creating a cloud event without a type.

+ @param ce:CloudEvent with source to be extracted. + @return:Returns the String value of a CloudEvent type attribute. + """ + return UCloudEvent.extract_string_value_from_attributes("type", ce) + + @staticmethod + def get_id(ce: CloudEvent) -> str: + """ + Extract the id from a cloud event. The id is a mandatory attribute. The CloudEvent constructor does + not allow creating a cloud event without an id.

+ @param ce:CloudEvent with source to be extracted. + @return:Returns the String value of a CloudEvent id attribute. + """ + return UCloudEvent.extract_string_value_from_attributes("id", ce) + + @staticmethod + def get_specversion(ce: CloudEvent) -> str: + """ + Extract the specversion from a cloud event.

+ @param ce:CloudEvent with source to be extracted. + @return:Returns the String value of a CloudEvent spec version attribute. + """ + return UCloudEvent.extract_string_value_from_attributes("specversion", ce) + + @staticmethod + def get_sink(ce: CloudEvent) -> str: + """ + Extract the sink from a cloud event. The sink attribute is optional.

+ @param ce:CloudEvent with sink to be extracted. + @return:Returns an Optional String value of a CloudEvent sink attribute if it exists, otherwise an + Optional.empty() is returned. + """ + return UCloudEvent.extract_string_value_from_attributes("sink", ce) + + @staticmethod + def get_request_id(ce: CloudEvent) -> str: + """ + Extract the request id from a cloud event that is a response RPC CloudEvent. The attribute is optional.

+ @param ce: the response RPC CloudEvent with request id to be extracted. + @return: Returns an Optional String value of a response RPC CloudEvent request id attribute if it exists, + otherwise an Optional.empty() is returned. + """ + return UCloudEvent.extract_string_value_from_attributes("reqid", ce) + + @staticmethod + def get_hash(ce: CloudEvent) -> str: + """ + Extract the hash attribute from a cloud event. The hash attribute is optional.

+ @param ce: CloudEvent with hash to be extracted. + @return:Returns an Optional String value of a CloudEvent hash attribute if it exists, otherwise an + Optional.empty() is returned. + """ + return UCloudEvent.extract_string_value_from_attributes("hash", ce) + + @staticmethod + def get_priority(ce: CloudEvent) -> str: + """ + Extract the string value of the priority attribute from a cloud event. The priority attribute is + optional.

+ @param ce:CloudEvent with priority to be extracted. + @return:Returns an Optional String value of a CloudEvent priority attribute if it exists, otherwise an + Optional.empty() is returned. + """ + return UCloudEvent.extract_string_value_from_attributes("priority", ce) + + @staticmethod + def get_ttl(ce: CloudEvent) -> int: + """ + Extract the integer value of the ttl attribute from a cloud event. The ttl attribute is optional.

+ @param ce:CloudEvent with ttl to be extracted. + @return: Returns an Optional String value of a CloudEvent ttl attribute if it exists,otherwise an + Optional.empty() is returned. + """ + ttl_str = UCloudEvent.extract_string_value_from_attributes("ttl", ce) + return int(ttl_str) if ttl_str is not None else None + + @staticmethod + def get_token(ce: CloudEvent) -> str: + """ + Extract the string value of the token attribute from a cloud event. The token attribute is optional.

+ @param ce: CloudEvent with token to be extracted. + @return:Returns an Optional String value of a CloudEvent priority token if it exists, otherwise an + Optional.empty() is returned. + """ + return UCloudEvent.extract_string_value_from_attributes("token", ce) + + @staticmethod + def get_communication_status(ce: CloudEvent) -> int: + """ + Extract the integer value of the communication status attribute from a cloud event. The communication status + attribute is optional. If there was a platform communication error that occurred while delivering this + cloudEvent, it will be indicated in this attribute. If the attribute does not exist, it is assumed that + everything was UCode.OK_VALUE.

+ @param ce: CloudEvent with the platformError to be extracted. + @return: Returns a {@link UCode} value that indicates of a platform communication error while delivering this + CloudEvent or UCode.OK_VALUE. + """ + comm_status = UCloudEvent.extract_string_value_from_attributes("commstatus", ce) + return int(comm_status) if comm_status is not None else UCode.OK + + @staticmethod + def has_communication_status_problem(ce: CloudEvent) -> bool: + """ + Indication of a platform communication error that occurred while trying to deliver the CloudEvent.

+ @param ce:CloudEvent to be queried for a platform delivery error. + @return:returns true if the provided CloudEvent is marked with having a platform delivery problem. + """ + return UCloudEvent.get_communication_status(ce) != 0 + + @staticmethod + def add_communication_status(ce: CloudEvent, communication_status) -> CloudEvent: + """ + Returns a new CloudEvent from the supplied CloudEvent, with the platform communication added.

+ @param ce:CloudEvent that the platform delivery error will be added. + @param communication_status:the platform delivery error UCode to add to the CloudEvent. + @return:Returns a new CloudEvent from the supplied CloudEvent, with the platform communication added. + """ + if communication_status is None: + return ce + ce.__setitem__("commstatus", communication_status) + return ce + + @staticmethod + def get_creation_timestamp(ce: CloudEvent) -> int: + """ + Extract the timestamp from the UUIDV8 CloudEvent Id.

+ @param ce:The CloudEvent with the timestamp to extract. + @return:Return the timestamp from the UUIDV8 CloudEvent Id or an empty Optional if timestamp can't be extracted. + """ + cloud_event_id = UCloudEvent.extract_string_value_from_attributes("id", ce) + uuid = LongUuidSerializer.instance().deserialize(cloud_event_id) + + return UUIDUtils.getTime(uuid) if uuid is not None else None + + @staticmethod + def is_expired_by_cloud_event_creation_date(ce: CloudEvent) -> bool: + """ + Calculate if a CloudEvent configured with a creation time and a ttl attribute is expired. The ttl attribute + is a configuration of how long this event should live for after it was generated (in milliseconds)

+ @param ce:The CloudEvent to inspect for being expired. + @return:Returns true if the CloudEvent was configured with a ttl > 0 and a creation time to compare for + expiration. + """ + maybe_ttl = UCloudEvent.get_ttl(ce) + if not maybe_ttl or maybe_ttl <= 0: + return False + + cloud_event_creation_time = UCloudEvent.extract_string_value_from_attributes("time", ce) + if cloud_event_creation_time is None: + return False + + now = datetime.now() + creation_time_plus_ttl = cloud_event_creation_time + timedelta(milliseconds=maybe_ttl) + + return now > creation_time_plus_ttl + + @staticmethod + def is_expired(ce: CloudEvent) -> bool: + """ + Calculate if a CloudEvent configured with UUIDv8 id and a ttl attribute is expired. The ttl attribute is a + configuration of how long this event should live for after it was generated (in milliseconds).

+ @param ce:The CloudEvent to inspect for being expired. + @return:Returns true if the CloudEvent was configured with a ttl > 0 and UUIDv8 id to compare for expiration. + """ + maybe_ttl = UCloudEvent.get_ttl(ce) + if not maybe_ttl or maybe_ttl <= 0: + return False + cloud_event_id = UCloudEvent.extract_string_value_from_attributes("id", ce) + + try: + uuid = LongUuidSerializer.instance().deserialize(cloud_event_id) + if uuid is None: + return False + delta = datetime.utcnow().timestamp() - UUIDUtils.getTime(uuid) + except ValueError: + # Invalid UUID, handle accordingly + delta = 0 + return delta >= maybe_ttl + + @staticmethod + def is_cloud_event_id(ce: CloudEvent) -> bool: + """ + Check if a CloudEvent is a valid UUIDv6 or v8 .

+ @param ce:The CloudEvent with the id to inspect. + @return: Returns true if the CloudEvent is valid. + """ + cloud_event_id = UCloudEvent.extract_string_value_from_attributes("id", ce) + uuid = LongUuidSerializer.instance().deserialize(cloud_event_id) + + return uuid is not None and UUIDUtils.isuuid(uuid) + + @staticmethod + def get_payload(ce: CloudEvent) -> any_pb2.Any: + """ + Extract the payload from the CloudEvent as a protobuf Any object.
An all or nothing error handling + strategy is implemented. If anything goes wrong, an Any.getDefaultInstance() will be returned.

+ @param ce:CloudEvent containing the payload to extract. + @return:Extracts the payload from a CloudEvent as a Protobuf Any object. + """ + data = ce.get_data() + if data is None: + return any_pb2.Any() + try: + return any_pb2.Any().FromString(data) + except DecodeError: + return any_pb2.Any() + + @staticmethod + def unpack(ce: CloudEvent, clazz): + """ + Extract the payload from the CloudEvent as a protobuf Message of the provided class. The protobuf of this + message class must be loaded on the client for this to work.
An all or nothing error handling strategy + is implemented. If anything goes wrong, an empty optional will be returned.

Example:
+
Optional<SomeMessage> unpacked = UCloudEvent.unpack(cloudEvent, SomeMessage.class);


+ @param ce:CloudEvent containing the payload to extract. + @param clazz:The class that extends {@link Message} that the payload is extracted into. + @return: Returns a {@link Message} payload of the class type that is provided. + """ + try: + any_obj=UCloudEvent.get_payload(ce) + value = clazz() + value.ParseFromString(any_obj.value) + return value + except DecodeError: + return None + + @staticmethod + def to_string(ce: CloudEvent) -> str: + """ + Function used to pretty print a CloudEvent containing only the id, source, type and maybe a sink. Used mainly + for logging.

+ @param ce:The CloudEvent we want to pretty print. + @return:returns the String representation of the CloudEvent containing only the id, source, type and maybe a + sink. + """ + if ce is not None: + sink_str = UCloudEvent.get_sink(ce) + sink_str = f", sink='{sink_str}'" if sink_str is not None else "" + id = UCloudEvent.extract_string_value_from_attributes("id", ce) + source = UCloudEvent.extract_string_value_from_attributes("source", ce) + type = UCloudEvent.extract_string_value_from_attributes("type", ce) + return f"CloudEvent{{id='{id}', source='{source}'{sink_str}, type='{type}'}}" + else: + return "null" + + @staticmethod + def extract_string_value_from_attributes(attr_name, ce: CloudEvent) -> str: + """ + Utility for extracting the String value of an attribute.

+ @param attr_name:The name of the CloudEvent attribute. + @param ce:The CloudEvent containing the data. + @return:the Optional String value of an attribute matching the attribute name, or an Optional.empty() is the + value does not exist. + """ + + return ce.get_attributes().get(attr_name) + + @staticmethod + def extract_integer_value_from_attributes(attr_name, ce: CloudEvent) -> int: + """ + + Utility for extracting the Integer value of an attribute.

+ @param attr_name:The name of the CloudEvent attribute. + @param ce:The CloudEvent containing the data. + @return:returns the Optional Integer value of an attribute matching the attribute name,or an Optional.empty() + is the value does not exist. + """ + value = UCloudEvent.extract_string_value_from_attributes(attr_name, ce) + return int(value) if value is not None else None + + @staticmethod + def get_event_type(type): + return {UMessageType.UMESSAGE_TYPE_PUBLISH: "pub.v1", UMessageType.UMESSAGE_TYPE_REQUEST: "req.v1", + UMessageType.UMESSAGE_TYPE_RESPONSE: "res.v1"}.get(type, "") + + @staticmethod + def get_message_type(ce_type): + return {"pub.v1": UMessageType.UMESSAGE_TYPE_PUBLISH, "req.v1": UMessageType.UMESSAGE_TYPE_REQUEST, + "res.v1": UMessageType.UMESSAGE_TYPE_RESPONSE}.get(ce_type, UMessageType.UMESSAGE_TYPE_UNSPECIFIED) diff --git a/uprotocol/cloudevent/serialize/__init__.py b/uprotocol/cloudevent/serialize/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/cloudevent/serialize/base64protobufserializer.py b/uprotocol/cloudevent/serialize/base64protobufserializer.py new file mode 100644 index 0000000..1bdf627 --- /dev/null +++ b/uprotocol/cloudevent/serialize/base64protobufserializer.py @@ -0,0 +1,58 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import base64 +from builtins import str + +from uprotocol.cloudevent.serialize.cloudeventserializer import CloudEventSerializer + + +class Base64ProtobufSerializer(CloudEventSerializer): + """ + Helper for serializing Base64 protobuf data. + """ + + def deserialize(self, proto_bytes: bytes) -> str: + """ + Deserialize a base64 protobuf payload into a Base64 String.

+ + @param proto_bytes: byte[] data + @return: Returns a String from the base64 protobuf payload. + """ + if proto_bytes is None: + return "" + return base64.b64encode(proto_bytes).decode('utf-8') # return base64.b64decode(proto_bytes).decode('utf-8') + + def serialize(self, string_to_serialize: str) -> bytes: + """ + Serialize a String into Base64 format.

+ @param string_to_serialize:String to serialize. + @return: Returns the Base64 formatted String as a byte[]. + """ + if string_to_serialize is None: + return bytearray() + return base64.b64decode(string_to_serialize.encode('utf-8')) diff --git a/uprotocol/cloudevent/serialize/cloudeventserializer.py b/uprotocol/cloudevent/serialize/cloudeventserializer.py new file mode 100644 index 0000000..adf77c8 --- /dev/null +++ b/uprotocol/cloudevent/serialize/cloudeventserializer.py @@ -0,0 +1,40 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod +from typing import Any + + +class CloudEventSerializer(ABC): + + @abstractmethod + def serialize(self, cloud_event: Any) -> bytes: + pass + + @abstractmethod + def deserialize(self, bytes_data: bytes) -> Any: + pass diff --git a/uprotocol/cloudevent/serialize/cloudeventserializers.py b/uprotocol/cloudevent/serialize/cloudeventserializers.py new file mode 100644 index 0000000..f812006 --- /dev/null +++ b/uprotocol/cloudevent/serialize/cloudeventserializers.py @@ -0,0 +1,42 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from enum import Enum + +from uprotocol.cloudevent.serialize.cloudeventtojsonserializer import CloudEventToJsonSerializer +from uprotocol.cloudevent.serialize.cloudeventtoprotobufserializer import CloudEventToProtobufSerializer + + +class CloudEventSerializers(Enum): + """ + Provides Singleton instances of the CloudEvent Serializers. + """ + JSON = CloudEventToJsonSerializer() + PROTOBUF = CloudEventToProtobufSerializer() + + def serializer(self): + return self.value diff --git a/uprotocol/cloudevent/serialize/cloudeventtojsonserializer.py b/uprotocol/cloudevent/serialize/cloudeventtojsonserializer.py new file mode 100644 index 0000000..c7aa57b --- /dev/null +++ b/uprotocol/cloudevent/serialize/cloudeventtojsonserializer.py @@ -0,0 +1,43 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from cloudevents.conversion import to_json +from cloudevents.http import CloudEvent, from_json + +from uprotocol.cloudevent.serialize.cloudeventserializer import CloudEventSerializer + + +class CloudEventToJsonSerializer(CloudEventSerializer): + """ + CloudEventSerializer to serialize and deserialize CloudEvents to JSON format. + """ + + def serialize(self, ce: CloudEvent) -> bytes: + return to_json(ce) + + def deserialize(self, bytes_data: bytes) -> CloudEvent: + return from_json(bytes_data) diff --git a/uprotocol/cloudevent/serialize/cloudeventtoprotobufserializer.py b/uprotocol/cloudevent/serialize/cloudeventtoprotobufserializer.py new file mode 100644 index 0000000..b594871 --- /dev/null +++ b/uprotocol/cloudevent/serialize/cloudeventtoprotobufserializer.py @@ -0,0 +1,104 @@ +# ------------------------------------------------------------------------- +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +from cloudevents.http import CloudEvent + +from uprotocol.cloudevent.serialize.cloudeventserializer import CloudEventSerializer +from uprotocol.proto import cloudevents_pb2 + + +# ToDo- convert cloud event to cloudevent proto +class CloudEventToProtobufSerializer(CloudEventSerializer): + """ + CloudEventSerializer to serialize and deserialize CloudEvents to protobuf format. + """ + + def __init__(self): + pass + + def serialize(self, http_event: CloudEvent) -> bytes: + proto_event = cloudevents_pb2.CloudEvent() + # Set required attributes + proto_event.id = http_event['id'] + proto_event.source = http_event['source'] + proto_event.spec_version = http_event['specversion'] + proto_event.type = http_event['type'] + + + # Set optional & extension attributes + for key, value in http_event.get_attributes().items(): + if key not in ['specversion', 'id', 'source', 'type', 'data']: + attribute_value = proto_event.attributes[key] + if isinstance(value, bool): + attribute_value.ce_boolean = value + elif isinstance(value, int): + attribute_value.ce_integer = value + elif isinstance(value, str): + attribute_value.ce_string = value + elif isinstance(value, bytes): + attribute_value.ce_bytes = value + + # Set data + data = http_event.get_data() + if isinstance(data, bytes): + proto_event.binary_data = data + elif isinstance(data, str): + proto_event.text_data = data + + return proto_event.SerializeToString() + + def deserialize(self, bytes_data: bytes) -> CloudEvent: + proto_event = cloudevents_pb2.CloudEvent() + proto_event.ParseFromString(bytes_data) + + json_attributes = {"id": proto_event.id, "source": proto_event.source, "type": proto_event.type, + "specversion": proto_event.spec_version} + + + + # Set optional & extension attributes + for key in proto_event.attributes: + if key not in ['specversion', 'id', 'source', 'type', 'data']: + attribute_value = proto_event.attributes[key] + if attribute_value.HasField('ce_boolean'): + json_attributes[key] = attribute_value.ce_boolean + elif attribute_value.HasField('ce_integer'): + json_attributes[key] = attribute_value.ce_integer + elif attribute_value.HasField('ce_string'): + json_attributes[key] = attribute_value.ce_string + elif attribute_value.HasField('ce_bytes'): + json_attributes[key] = attribute_value.ce_bytes + + # Set data + data = bytearray() + if proto_event.HasField('binary_data'): + data = proto_event.binary_data + elif proto_event.HasField('text_data'): + data = proto_event.text_data + + cloud_event = CloudEvent(json_attributes, data) + + return cloud_event diff --git a/uprotocol/cloudevent/validate/__init__.py b/uprotocol/cloudevent/validate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/cloudevent/validate/cloudeventvalidator.py b/uprotocol/cloudevent/validate/cloudeventvalidator.py new file mode 100644 index 0000000..06f74b9 --- /dev/null +++ b/uprotocol/cloudevent/validate/cloudeventvalidator.py @@ -0,0 +1,365 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod +from enum import Enum + +from cloudevents.http import CloudEvent + +from uprotocol.cloudevent.factory.ucloudevent import UCloudEvent +from uprotocol.proto.uattributes_pb2 import UMessageType +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.uri.serializer.longuriserializer import LongUriSerializer +from uprotocol.uri.validator.urivalidator import UriValidator +from uprotocol.validation.validationresult import ValidationResult + + +class CloudEventValidator(ABC): + + @staticmethod + def get_validator(ce: CloudEvent): + """ + Obtain a CloudEventValidator according to the type attribute in the CloudEvent.

+ @param ce:The CloudEvent with the type attribute. + @return:Returns a CloudEventValidator according to the type attribute in the CloudEvent. + """ + cloud_event_type = ce.get_attributes().get("type") + + if cloud_event_type is None or not cloud_event_type: + return Validators.PUBLISH.validator() + + message_type = UCloudEvent.get_message_type(cloud_event_type) + + if message_type == UMessageType.UMESSAGE_TYPE_RESPONSE: + return Validators.RESPONSE.validator() + elif message_type == UMessageType.UMESSAGE_TYPE_REQUEST: + return Validators.REQUEST.validator() + else: + return Validators.PUBLISH.validator() + + def validate(self, ce: CloudEvent) -> ValidationResult: + """ + Validate the CloudEvent. A CloudEventValidator instance is obtained according to the type attribute on the + CloudEvent.

+ @param ce:The CloudEvent to validate. + @return:Returns a ValidationResult with success or a ValidationResult with failure containing all the + errors that were found. + """ + validation_results = [self.validate_version(ce), self.validate_id(ce), self.validate_source(ce), + self.validate_type(ce), self.validate_sink(ce)] + + error_messages = [result.get_message() for result in validation_results if not result.is_success()] + error_message = ",".join(error_messages) + + if not error_message: + return ValidationResult.success() + else: + return ValidationResult.failure(",".join(error_messages)) + + @staticmethod + def validate_version(ce: CloudEvent) -> ValidationResult: + return CloudEventValidator.validate_version_spec(ce.get_attributes().get("specversion")) + + @staticmethod + def validate_version_spec(version) -> ValidationResult: + if version == "1.0": + return ValidationResult.success() + else: + return ValidationResult.failure(f"Invalid CloudEvent version [{version}]. CloudEvent version must be 1.0.") + + @staticmethod + def validate_id(ce: CloudEvent) -> ValidationResult: + id = UCloudEvent.extract_string_value_from_attributes("id", ce) + return (ValidationResult.success() if UCloudEvent.is_cloud_event_id(ce) else ValidationResult.failure( + f"Invalid CloudEvent Id [{id}]. CloudEvent Id must be of type UUIDv8.")) + + @abstractmethod + def validate_source(self, ce: CloudEvent): + """ + Validate the source value of a cloud event.

+ @param ce:The cloud event containing the source to validate. + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + raise NotImplementedError("Subclasses must implement this method") + + @abstractmethod + def validate_type(self, ce: CloudEvent): + """ + Validate the type value of a cloud event.

+ @param ce:The cloud event containing the type to validate. + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + raise NotImplementedError("Subclasses must implement this method") + + def validate_sink(self, ce: CloudEvent) -> ValidationResult: + """ + Validate the sink value of a cloud event in the default scenario where the sink attribute is optional.

+ @param ce:The cloud event containing the sink to validate. + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + maybe_sink = UCloudEvent.get_sink(ce) + if maybe_sink: + sink = maybe_sink + check_sink = self.validate_u_entity_uri(sink) + if check_sink.is_failure(): + return ValidationResult.failure(f"Invalid CloudEvent sink [{sink}]. {check_sink.get_message()}") + + return ValidationResult.success() + + @staticmethod + def validate_u_entity_uri(uri: str) -> ValidationResult: + """ + Validate an UriPart for an Software Entity must have an authority in the case of a microRemote uri, + and must contain the name of the USE.

+ @param uri:uri string to validate. + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + uri = LongUriSerializer().deserialize(uri) + return CloudEventValidator.validate_u_entity_uri_from_UURI(uri) + + @staticmethod + def validate_u_entity_uri_from_UURI(uri: UUri) -> ValidationResult: + return UriValidator.validate(uri) + + + @staticmethod + def validate_topic_uri(uri: str) -> ValidationResult: + """ + Validate a UriPart that is to be used as a topic in publish scenarios for events such as publish, + file and notification.

+ @param uri:String UriPart to validate + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + Uri = LongUriSerializer().deserialize(uri) + return CloudEventValidator.validate_topic_uri_from_UURI(Uri) + + @staticmethod + def validate_topic_uri_from_UURI(uri: UUri) -> ValidationResult: + """ + Validate a UriPart that is to be used as a topic in publish scenarios for events such as publish, + file and notification.

+ @param uri: UriPart to validate. + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + validationResult = CloudEventValidator.validate_u_entity_uri_from_UURI(uri) + if validationResult.is_failure(): + return validationResult + + u_resource = uri.resource + if not u_resource.name: + return ValidationResult.failure("UriPart is missing uResource name.") + + if not u_resource.message: + return ValidationResult.failure("UriPart is missing Message information.") + + return ValidationResult.success() + + @staticmethod + def validate_rpc_topic_uri(uri: str) -> ValidationResult: + """ + Validate a UriPart that is meant to be used as the application response topic for rpc calls.
Used in + Request source values and Response sink values.

+ @param uri:String UriPart to validate. + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + Uri = LongUriSerializer().deserialize(uri) + return CloudEventValidator.validate_rpc_topic_uri_from_uuri(Uri) + + @staticmethod + def validate_rpc_topic_uri_from_uuri(uri: UUri) -> ValidationResult: + """ + Validate a UriPart that is meant to be used as the application response topic for rpc calls.
Used in + Request source values and Response sink values.

+ @param uri:UriPart to validate. + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + validationResult = CloudEventValidator.validate_u_entity_uri_from_UURI(uri) + if validationResult.is_failure(): + return ValidationResult.failure( + f"Invalid RPC uri application response topic. {validationResult.get_message()}") + + u_resource = uri.resource + topic = f"{u_resource.name}.{u_resource.instance}" if u_resource.instance else f"{u_resource.name}" + if topic != "rpc.response": + return ValidationResult.failure("Invalid RPC uri application response topic. UriPart is missing rpc.response.") + + return ValidationResult.success() + + @staticmethod + def validate_rpc_method(uri: str) -> ValidationResult: + """ + Validate a UriPart that is meant to be used as an RPC method URI. Used in Request sink values and Response + source values.

+ @param uri: String UriPart to validate + @return:Returns the ValidationResult containing a success or a failure with the error message. + """ + uuri = LongUriSerializer().deserialize(uri) + validationResult = CloudEventValidator.validate_u_entity_uri_from_UURI(uuri) + if validationResult.is_failure(): + return ValidationResult.failure(f"Invalid RPC method uri. {validationResult.get_message()}") + + if not UriValidator.is_rpc_method(uuri): + return ValidationResult.failure( + "Invalid RPC method uri. UriPart should be the method to be called, or method from response.") + + return ValidationResult.success() + + +class Publish(CloudEventValidator): + """ + Implements Validations for a CloudEvent of type Publish. + """ + + def validate_source(self, cl_event: CloudEvent) -> ValidationResult: + source = cl_event.get_attributes().get("source") + check_source = self.validate_topic_uri(source) + if check_source.is_failure(): + return ValidationResult.failure( + f"Invalid Publish type CloudEvent source [{source}]. {check_source.get_message()}") + + return ValidationResult.success() + + def validate_type(self, cl_event: CloudEvent) -> ValidationResult: + type = cl_event.get_attributes().get("type") + return (ValidationResult.success() if type == "pub.v1" else ValidationResult.failure( + f"Invalid CloudEvent type [{type}]. CloudEvent of type Publish must have a type of 'pub.v1'")) + + def __str__(self) -> str: + return "CloudEventValidator.Publish" + + +class Notification(Publish): + """ + Implements Validations for a CloudEvent of type Publish that behaves as a Notification, meaning it must have a sink. + """ + + def validate_sink(self, cl_event: CloudEvent) -> ValidationResult: + maybe_sink = UCloudEvent.get_sink(cl_event) + if not maybe_sink: + return ValidationResult.failure("Invalid CloudEvent sink. Notification CloudEvent sink must be an uri.") + else: + sink = maybe_sink + check_sink = self.validate_u_entity_uri(sink) + if check_sink.is_failure(): + return ValidationResult.failure( + f"Invalid Notification type CloudEvent sink [{sink}]. {check_sink.get_message()}") + + return ValidationResult.success() + + def __str__(self): + return "CloudEventValidator.Notification" + + +class Request(CloudEventValidator): + """ + Implements Validations for a CloudEvent for RPC Request. + """ + + def validate_source(self, cl_event: CloudEvent) -> ValidationResult: + source = cl_event.get_attributes().get("source") + check_source = self.validate_rpc_topic_uri(source) + if check_source.is_failure(): + return ValidationResult.failure( + f"Invalid RPC Request CloudEvent source [{source}]. {check_source.get_message()}") + return ValidationResult.success() + + def validate_sink(self, cl_event: CloudEvent) -> ValidationResult: + maybe_sink = UCloudEvent.get_sink(cl_event) + if not maybe_sink: + return ValidationResult.failure( + "Invalid RPC Request CloudEvent sink. Request CloudEvent sink must be uri for the method to be called.") + else: + sink = maybe_sink + check_sink = self.validate_rpc_method(sink) + if check_sink.is_failure(): + return ValidationResult.failure( + f"Invalid RPC Request CloudEvent sink [{sink}]. {check_sink.get_message()}") + + return ValidationResult.success() + + def validate_type(self, cl_event: CloudEvent) -> ValidationResult: + type = cl_event.get_attributes().get("type") + + return (ValidationResult.success() if type == "req.v1" else ValidationResult.failure( + f"Invalid CloudEvent type [{type}]. CloudEvent of type Request must have a type of 'req.v1'")) + + def __str__(self): + return "CloudEventValidator.Request" + + +class Response(CloudEventValidator): + """ + Implements Validations for a CloudEvent for RPC Response. + """ + + def validate_source(self, cl_event: CloudEvent) -> ValidationResult: + source = cl_event.get_attributes().get("source") + check_source = self.validate_rpc_method(source) + if check_source.is_failure(): + return ValidationResult.failure( + f"Invalid RPC Response CloudEvent source [{source}]. {check_source.get_message()}") + + return ValidationResult.success() + + def validate_sink(self, cl_event) -> ValidationResult: + maybe_sink = UCloudEvent.get_sink(cl_event) + if not maybe_sink: + return ValidationResult.failure( + "Invalid CloudEvent sink. Response CloudEvent sink must be uri the destination of the response.") + else: + sink = maybe_sink + check_sink = self.validate_rpc_topic_uri(sink) + if check_sink.is_failure(): + return ValidationResult.failure( + f"Invalid RPC Response CloudEvent sink [{sink}]. {check_sink.get_message()}") + + return ValidationResult.success() + + def validate_type(self, cl_event: CloudEvent) -> ValidationResult: + type = cl_event.get_attributes().get("type") + + return (ValidationResult.success() if type == "res.v1" else ValidationResult.failure( + f"Invalid CloudEvent type [{type}]. CloudEvent of type Response must have a type of 'res.v1'")) + + def __str__(self): + return "CloudEventValidator.Response" + + +class Validators(Enum): + """ + Enum that hold the implementations of CloudEventValidator according to type. + """ + PUBLISH = Publish() + NOTIFICATION = Notification() + REQUEST = Request() + RESPONSE = Response() + + def __init__(self, cloud_event_validator: CloudEventValidator): + self.cloud_event_validator = cloud_event_validator + + def validator(self) -> CloudEventValidator: + return self.cloud_event_validator diff --git a/uprotocol/proto/__init__.py b/uprotocol/proto/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/proto/cloudevents_pb2.py b/uprotocol/proto/cloudevents_pb2.py new file mode 100644 index 0000000..f84ea9a --- /dev/null +++ b/uprotocol/proto/cloudevents_pb2.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: cloudevents.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 +from google.protobuf import timestamp_pb2 as google_dot_protobuf_dot_timestamp__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11\x63loudevents.proto\x12\x11io.cloudevents.v1\x1a\x19google/protobuf/any.proto\x1a\x1fgoogle/protobuf/timestamp.proto\"\xb0\x04\n\nCloudEvent\x12\n\n\x02id\x18\x01 \x01(\t\x12\x0e\n\x06source\x18\x02 \x01(\t\x12\x14\n\x0cspec_version\x18\x03 \x01(\t\x12\x0c\n\x04type\x18\x04 \x01(\t\x12\x41\n\nattributes\x18\x05 \x03(\x0b\x32-.io.cloudevents.v1.CloudEvent.AttributesEntry\x12\x15\n\x0b\x62inary_data\x18\x06 \x01(\x0cH\x00\x12\x13\n\ttext_data\x18\x07 \x01(\tH\x00\x12*\n\nproto_data\x18\x08 \x01(\x0b\x32\x14.google.protobuf.AnyH\x00\x1ai\n\x0f\x41ttributesEntry\x12\x0b\n\x03key\x18\x01 \x01(\t\x12\x45\n\x05value\x18\x02 \x01(\x0b\x32\x36.io.cloudevents.v1.CloudEvent.CloudEventAttributeValue:\x02\x38\x01\x1a\xd3\x01\n\x18\x43loudEventAttributeValue\x12\x14\n\nce_boolean\x18\x01 \x01(\x08H\x00\x12\x14\n\nce_integer\x18\x02 \x01(\x05H\x00\x12\x13\n\tce_string\x18\x03 \x01(\tH\x00\x12\x12\n\x08\x63\x65_bytes\x18\x04 \x01(\x0cH\x00\x12\x10\n\x06\x63\x65_uri\x18\x05 \x01(\tH\x00\x12\x14\n\nce_uri_ref\x18\x06 \x01(\tH\x00\x12\x32\n\x0c\x63\x65_timestamp\x18\x07 \x01(\x0b\x32\x1a.google.protobuf.TimestampH\x00\x42\x06\n\x04\x61ttrB\x06\n\x04\x64\x61ta\"@\n\x0f\x43loudEventBatch\x12-\n\x06\x65vents\x18\x01 \x03(\x0b\x32\x1d.io.cloudevents.v1.CloudEventB\x8b\x01\n\x17io.cloudevents.v1.protoP\x01Z\x1a\x63loudevents.io/genproto/v1\xaa\x02\x1a\x43loudNative.CloudEvents.V1\xca\x02\x17Io\\CloudEvents\\V1\\Proto\xea\x02\x1aIo::CloudEvents::V1::Protob\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'cloudevents_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\027io.cloudevents.v1.protoP\001Z\032cloudevents.io/genproto/v1\252\002\032CloudNative.CloudEvents.V1\312\002\027Io\\CloudEvents\\V1\\Proto\352\002\032Io::CloudEvents::V1::Proto' + _globals['_CLOUDEVENT_ATTRIBUTESENTRY']._options = None + _globals['_CLOUDEVENT_ATTRIBUTESENTRY']._serialized_options = b'8\001' + _globals['_CLOUDEVENT']._serialized_start=101 + _globals['_CLOUDEVENT']._serialized_end=661 + _globals['_CLOUDEVENT_ATTRIBUTESENTRY']._serialized_start=334 + _globals['_CLOUDEVENT_ATTRIBUTESENTRY']._serialized_end=439 + _globals['_CLOUDEVENT_CLOUDEVENTATTRIBUTEVALUE']._serialized_start=442 + _globals['_CLOUDEVENT_CLOUDEVENTATTRIBUTEVALUE']._serialized_end=653 + _globals['_CLOUDEVENTBATCH']._serialized_start=663 + _globals['_CLOUDEVENTBATCH']._serialized_end=727 +# @@protoc_insertion_point(module_scope) diff --git a/uprotocol/proto/uattributes_pb2.py b/uprotocol/proto/uattributes_pb2.py new file mode 100644 index 0000000..c98292d --- /dev/null +++ b/uprotocol/proto/uattributes_pb2.py @@ -0,0 +1,32 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: uattributes.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +import uprotocol.proto.uri_pb2 as uri__pb2 +import uprotocol.proto.uuid_pb2 as uuid__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x11uattributes.proto\x12\x0cuprotocol.v1\x1a\turi.proto\x1a\nuuid.proto\"\xf8\x02\n\x0bUAttributes\x12\x1e\n\x02id\x18\x01 \x01(\x0b\x32\x12.uprotocol.v1.UUID\x12(\n\x04type\x18\x02 \x01(\x0e\x32\x1a.uprotocol.v1.UMessageType\x12%\n\x04sink\x18\x03 \x01(\x0b\x32\x12.uprotocol.v1.UUriH\x00\x88\x01\x01\x12)\n\x08priority\x18\x04 \x01(\x0e\x32\x17.uprotocol.v1.UPriority\x12\x10\n\x03ttl\x18\x05 \x01(\x05H\x01\x88\x01\x01\x12\x1d\n\x10permission_level\x18\x06 \x01(\x05H\x02\x88\x01\x01\x12\x17\n\ncommstatus\x18\x07 \x01(\x05H\x03\x88\x01\x01\x12&\n\x05reqid\x18\x08 \x01(\x0b\x32\x12.uprotocol.v1.UUIDH\x04\x88\x01\x01\x12\x12\n\x05token\x18\t \x01(\tH\x05\x88\x01\x01\x42\x07\n\x05_sinkB\x06\n\x04_ttlB\x13\n\x11_permission_levelB\r\n\x0b_commstatusB\x08\n\x06_reqidB\x08\n\x06_token*\x7f\n\x0cUMessageType\x12\x1d\n\x19UMESSAGE_TYPE_UNSPECIFIED\x10\x00\x12\x19\n\x15UMESSAGE_TYPE_PUBLISH\x10\x01\x12\x19\n\x15UMESSAGE_TYPE_REQUEST\x10\x02\x12\x1a\n\x16UMESSAGE_TYPE_RESPONSE\x10\x03*\xab\x01\n\tUPriority\x12\x19\n\x15UPRIORITY_UNSPECIFIED\x10\x00\x12\x11\n\rUPRIORITY_CS0\x10\x01\x12\x11\n\rUPRIORITY_CS1\x10\x02\x12\x11\n\rUPRIORITY_CS2\x10\x03\x12\x11\n\rUPRIORITY_CS3\x10\x04\x12\x11\n\rUPRIORITY_CS4\x10\x05\x12\x11\n\rUPRIORITY_CS5\x10\x06\x12\x11\n\rUPRIORITY_CS6\x10\x07\x42.\n\x18org.eclipse.uprotocol.v1B\x10UAttributesProtoP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'uattributes_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\030org.eclipse.uprotocol.v1B\020UAttributesProtoP\001' + _globals['_UMESSAGETYPE']._serialized_start=437 + _globals['_UMESSAGETYPE']._serialized_end=564 + _globals['_UPRIORITY']._serialized_start=567 + _globals['_UPRIORITY']._serialized_end=738 + _globals['_UATTRIBUTES']._serialized_start=59 + _globals['_UATTRIBUTES']._serialized_end=435 +# @@protoc_insertion_point(module_scope) diff --git a/uprotocol/proto/upayload_pb2.py b/uprotocol/proto/upayload_pb2.py new file mode 100644 index 0000000..c9d638d --- /dev/null +++ b/uprotocol/proto/upayload_pb2.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: upayload.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x0eupayload.proto\x12\x0cuprotocol.v1\"\x86\x01\n\x08UPayload\x12\x13\n\treference\x18\x01 \x01(\x06H\x00\x12\x0f\n\x05value\x18\x02 \x01(\x0cH\x00\x12\x13\n\x06length\x18\x03 \x01(\x05H\x01\x88\x01\x01\x12,\n\x06\x66ormat\x18\x04 \x01(\x0e\x32\x1c.uprotocol.v1.UPayloadFormatB\x06\n\x04\x64\x61taB\t\n\x07_length*\xd8\x01\n\x0eUPayloadFormat\x12\x1f\n\x1bUPAYLOAD_FORMAT_UNSPECIFIED\x10\x00\x12\x1c\n\x18UPAYLOAD_FORMAT_PROTOBUF\x10\x01\x12\x18\n\x14UPAYLOAD_FORMAT_JSON\x10\x02\x12\x1a\n\x16UPAYLOAD_FORMAT_SOMEIP\x10\x03\x12\x1e\n\x1aUPAYLOAD_FORMAT_SOMEIP_TLV\x10\x04\x12\x17\n\x13UPAYLOAD_FORMAT_RAW\x10\x05\x12\x18\n\x14UPAYLOAD_FORMAT_TEXT\x10\x06\x42+\n\x18org.eclipse.uprotocol.v1B\rUPayloadProtoP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'upayload_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\030org.eclipse.uprotocol.v1B\rUPayloadProtoP\001' + _globals['_UPAYLOADFORMAT']._serialized_start=170 + _globals['_UPAYLOADFORMAT']._serialized_end=386 + _globals['_UPAYLOAD']._serialized_start=33 + _globals['_UPAYLOAD']._serialized_end=167 +# @@protoc_insertion_point(module_scope) diff --git a/uprotocol/proto/uri_pb2.py b/uprotocol/proto/uri_pb2.py new file mode 100644 index 0000000..5b475e4 --- /dev/null +++ b/uprotocol/proto/uri_pb2.py @@ -0,0 +1,34 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: uri.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\turi.proto\x12\x0cuprotocol.v1\"\x85\x01\n\x04UUri\x12+\n\tauthority\x18\x01 \x01(\x0b\x32\x18.uprotocol.v1.UAuthority\x12%\n\x06\x65ntity\x18\x02 \x01(\x0b\x32\x15.uprotocol.v1.UEntity\x12)\n\x08resource\x18\x03 \x01(\x0b\x32\x17.uprotocol.v1.UResource\"B\n\nUAuthority\x12\x0e\n\x04name\x18\x01 \x01(\tH\x00\x12\x0c\n\x02ip\x18\x02 \x01(\x0cH\x00\x12\x0c\n\x02id\x18\x03 \x01(\x0cH\x00\x42\x08\n\x06remote\"\x8b\x01\n\x07UEntity\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x0f\n\x02id\x18\x02 \x01(\rH\x00\x88\x01\x01\x12\x1a\n\rversion_major\x18\x03 \x01(\rH\x01\x88\x01\x01\x12\x1a\n\rversion_minor\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x05\n\x03_idB\x10\n\x0e_version_majorB\x10\n\x0e_version_minor\"w\n\tUResource\x12\x0c\n\x04name\x18\x01 \x01(\t\x12\x15\n\x08instance\x18\x02 \x01(\tH\x00\x88\x01\x01\x12\x14\n\x07message\x18\x03 \x01(\tH\x01\x88\x01\x01\x12\x0f\n\x02id\x18\x04 \x01(\rH\x02\x88\x01\x01\x42\x0b\n\t_instanceB\n\n\x08_messageB\x05\n\x03_id\"-\n\tUUriBatch\x12 \n\x04uris\x18\x01 \x03(\x0b\x32\x12.uprotocol.v1.UUriB\'\n\x18org.eclipse.uprotocol.v1B\tUUriProtoP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'uri_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\030org.eclipse.uprotocol.v1B\tUUriProtoP\001' + _globals['_UURI']._serialized_start=28 + _globals['_UURI']._serialized_end=161 + _globals['_UAUTHORITY']._serialized_start=163 + _globals['_UAUTHORITY']._serialized_end=229 + _globals['_UENTITY']._serialized_start=232 + _globals['_UENTITY']._serialized_end=371 + _globals['_URESOURCE']._serialized_start=373 + _globals['_URESOURCE']._serialized_end=492 + _globals['_UURIBATCH']._serialized_start=494 + _globals['_UURIBATCH']._serialized_end=539 +# @@protoc_insertion_point(module_scope) diff --git a/uprotocol/proto/ustatus_pb2.py b/uprotocol/proto/ustatus_pb2.py new file mode 100644 index 0000000..30b68f2 --- /dev/null +++ b/uprotocol/proto/ustatus_pb2.py @@ -0,0 +1,29 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: ustatus.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + +from google.protobuf import any_pb2 as google_dot_protobuf_dot_any__pb2 + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\rustatus.proto\x12\x0cuprotocol.v1\x1a\x19google/protobuf/any.proto\"u\n\x07UStatus\x12!\n\x04\x63ode\x18\x01 \x01(\x0e\x32\x13.uprotocol.v1.UCode\x12\x14\n\x07message\x18\x02 \x01(\tH\x00\x88\x01\x01\x12%\n\x07\x64\x65tails\x18\x03 \x03(\x0b\x32\x14.google.protobuf.AnyB\n\n\x08_message*\xb8\x02\n\x05UCode\x12\x06\n\x02OK\x10\x00\x12\r\n\tCANCELLED\x10\x01\x12\x0b\n\x07UNKNOWN\x10\x02\x12\x14\n\x10INVALID_ARGUMENT\x10\x03\x12\x15\n\x11\x44\x45\x41\x44LINE_EXCEEDED\x10\x04\x12\r\n\tNOT_FOUND\x10\x05\x12\x12\n\x0e\x41LREADY_EXISTS\x10\x06\x12\x15\n\x11PERMISSION_DENIED\x10\x07\x12\x13\n\x0fUNAUTHENTICATED\x10\x10\x12\x16\n\x12RESOURCE_EXHAUSTED\x10\x08\x12\x17\n\x13\x46\x41ILED_PRECONDITION\x10\t\x12\x0b\n\x07\x41\x42ORTED\x10\n\x12\x10\n\x0cOUT_OF_RANGE\x10\x0b\x12\x11\n\rUNIMPLEMENTED\x10\x0c\x12\x0c\n\x08INTERNAL\x10\r\x12\x0f\n\x0bUNAVAILABLE\x10\x0e\x12\r\n\tDATA_LOSS\x10\x0f\x42*\n\x18org.eclipse.uprotocol.v1B\x0cUStatusProtoP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'ustatus_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\030org.eclipse.uprotocol.v1B\014UStatusProtoP\001' + _globals['_UCODE']._serialized_start=178 + _globals['_UCODE']._serialized_end=490 + _globals['_USTATUS']._serialized_start=58 + _globals['_USTATUS']._serialized_end=175 +# @@protoc_insertion_point(module_scope) diff --git a/uprotocol/proto/uuid_pb2.py b/uprotocol/proto/uuid_pb2.py new file mode 100644 index 0000000..d321cd4 --- /dev/null +++ b/uprotocol/proto/uuid_pb2.py @@ -0,0 +1,26 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# source: uuid.proto +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\nuuid.proto\x12\x0cuprotocol.v1\" \n\x04UUID\x12\x0b\n\x03msb\x18\x01 \x01(\x06\x12\x0b\n\x03lsb\x18\x02 \x01(\x06\x42\'\n\x18org.eclipse.uprotocol.v1B\tUUIRProtoP\x01\x62\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'uuid_pb2', _globals) +if _descriptor._USE_C_DESCRIPTORS == False: + _globals['DESCRIPTOR']._options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\030org.eclipse.uprotocol.v1B\tUUIRProtoP\001' + _globals['_UUID']._serialized_start=28 + _globals['_UUID']._serialized_end=60 +# @@protoc_insertion_point(module_scope) diff --git a/uprotocol/rpc/README.adoc b/uprotocol/rpc/README.adoc new file mode 100644 index 0000000..eb3a6cb --- /dev/null +++ b/uprotocol/rpc/README.adoc @@ -0,0 +1,8 @@ += uProtocol Rpc Interfaces +:toc: +:sectnums: + +== Overview + +The following module declares the https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/up-l2/rpcclient.adoc[RpcClient interface] defined in uProtocol specification. The interface is used by code generators to build client and service stubs for uServices. + diff --git a/uprotocol/rpc/__init__.py b/uprotocol/rpc/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/rpc/calloptions.py b/uprotocol/rpc/calloptions.py new file mode 100644 index 0000000..954335c --- /dev/null +++ b/uprotocol/rpc/calloptions.py @@ -0,0 +1,102 @@ +# ------------------------------------------------------------------------- +# +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 +# +# ------------------------------------------------------------------------- + + +class CallOptions: + """ + This class is used when making uRPC calls to pass additional options. + """ + TIMEOUT_DEFAULT = 10000 + """ + Default timeout of a call in milliseconds. + """ + + + def __init__(self, timeout=TIMEOUT_DEFAULT, token=""): + self.mTimeout = timeout + self.mToken = token if token else "" + + + def get_timeout(self): + """ + Get a timeout.

+ @return: A timeout in milliseconds. + """ + return self.mTimeout + + def get_token(self): + """ + Get an OAuth2 access token.

+ @return: An Optional OAuth2 access token. + """ + return self.mToken if self.mToken else None + + def __eq__(self, other): + if not isinstance(other, CallOptions): + return False + return self.mTimeout == other.mTimeout and self.mToken == other.mToken + + def __hash__(self): + return hash((self.mTimeout, self.mToken)) + + def __str__(self): + return f"CallOptions{{mTimeout={self.mTimeout}, mToken='{self.mToken}'}}" + + +class CallOptionsBuilder: + """ + Builder for constructing CallOptions. + """ + TIMEOUT_DEFAULT = 10000 + DEFAULT= CallOptions(TIMEOUT_DEFAULT,'') + def __init__(self): + self.mTimeout = self.TIMEOUT_DEFAULT + self.mToken = "" + + def with_timeout(self, timeout): + """ + Add a timeout.

+ @param timeout:A timeout in milliseconds. + @return:This builder. + """ + self.mTimeout = timeout if timeout > 0 else self.TIMEOUT_DEFAULT + return self + + def with_token(self, token): + """ + Add an OAuth2 access token.

+ @param token:An OAuth2 access token. + @return:This builder. + """ + self.mToken = token + return self + + def build(self): + """ + Construct a CallOptions from this builder.

+ @return:A constructed CallOptions. + """ + return CallOptions(self.mTimeout, self.mToken) diff --git a/uprotocol/rpc/rpcclient.py b/uprotocol/rpc/rpcclient.py new file mode 100644 index 0000000..1f16833 --- /dev/null +++ b/uprotocol/rpc/rpcclient.py @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod +from concurrent.futures import Future + +from uprotocol.proto.uattributes_pb2 import UAttributes +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.proto.upayload_pb2 import UPayload + + +class RpcClient(ABC): + """ + RpcClient is an interface used by code generators for uProtocol services defined in proto files such as the core + uProtocol services found in here.
The + interface provides a + clean contract for all transports to implement to be able to support RPC on their platform.
Each platform MUST + implement this interface.
For more details please refer to
+ [RpcClient + Specifications] + """ + + @abstractmethod + def invoke_method(self, topic: UUri, payload: UPayload, attributes: UAttributes) -> Future: + """ + Support for RPC method invocation.

+ + @param topic: topic of the method to be invoked (i.e. the name of the API we are calling). + @param payload:The request message to be sent to the server. + @param attributes: metadata for the method invocation (i.e. priority, timeout, etc.) + @return: Returns the CompletableFuture with the result or exception. + """ + pass diff --git a/uprotocol/rpc/rpcmapper.py b/uprotocol/rpc/rpcmapper.py new file mode 100644 index 0000000..dcee680 --- /dev/null +++ b/uprotocol/rpc/rpcmapper.py @@ -0,0 +1,152 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from concurrent.futures import Future + +from google.protobuf import any_pb2 +from uprotocol.proto.ustatus_pb2 import UCode +from uprotocol.proto.ustatus_pb2 import UStatus + +from uprotocol.rpc.rpcresult import RpcResult +from uprotocol.proto.upayload_pb2 import UPayload + + +class RpcMapper: + """ + RPC Wrapper is an interface that provides static methods to be able to wrap an RPC request with an RPC Response ( + uP-L2). APIs that return Message assumes that the payload is protobuf serialized com.google.protobuf.Any ( + UPayloadFormat.PROTOBUF) and will barf if anything else is passed + """ + + @staticmethod + def map_response(payload_future: Future, expected_cls): + """ + Map a response of CompletableFuture<UPayload> from Link into a CompletableFuture containing the + declared expected return type of the RPC method or an exception.

+ @param response_future:CompletableFuture<UPayload> response from uTransport. + @param expected_cls:The class name of the declared expected return type of the RPC method. + @return:Returns a CompletableFuture containing the declared expected return type of the RPC method or an + exception. + """ + response_future: Future = Future() + + def handle_response(payload): + nonlocal response_future + payload = payload.result() + if not payload: + response_future.set_exception( + RuntimeError(f"Server returned a null payload. Expected {expected_cls.__name__}")) + + try: + any_message = any_pb2.Any() + any_message.ParseFromString(payload.value) + if any_message.Is(expected_cls.DESCRIPTOR): + response_future.set_result(RpcMapper.unpack_payload(any_message, expected_cls)) + else: + response_future.set_exception( + RuntimeError( + f"Unknown payload type [{any_message.type_url}]. Expected [{expected_cls.__name__}]")) + + except Exception as e: + response_future.set_exception(RuntimeError(f"{str(e)} [{UStatus.__name__}]")) + + payload_future.add_done_callback(handle_response) + + return response_future + + @staticmethod + def map_response_to_result(response_future: Future, expected_cls): + """ + Map a response of CompletableFuture<Any> from Link into a CompletableFuture containing an RpcResult + containing the declared expected return type T, or a UStatus containing any errors.

+ @param response_future:CompletableFuture<Any> response from Link. + @param expected_cls:The class name of the declared expected return type of the RPC method. + @return:Returns a CompletableFuture containing an RpcResult containing the declared expected return type T, + or a UStatus containing any errors. + """ + + def handle_response(payload): + if payload.exception(): + exception = payload.exception() + return RpcResult.failure(value=exception, message=str(exception)) + + payload = payload.result() + if not payload: + exception = RuntimeError(f"Server returned a null payload. Expected {expected_cls.__name__}") + return RpcResult.failure(value=exception, message=str(exception)) + + try: + any_message = any_pb2.Any() + any_message.ParseFromString(payload.value) + + if any_message.Is(expected_cls.DESCRIPTOR): + if expected_cls == UStatus: + return RpcMapper.calculate_status_result(any_message) + else: + return RpcResult.success(RpcMapper.unpack_payload(any_message, expected_cls)) + + if any_message.Is(UStatus.DESCRIPTOR): + return RpcMapper.calculate_status_result(any_message) + except Exception as e: + exception = RuntimeError(f"{str(e)} [{UStatus.__name__}]") + return RpcResult.failure(value=exception, message=str(exception)) + + exception = RuntimeError( + f"Unknown payload type [{any_message.type_url}]. Expected [{expected_cls.DESCRIPTOR.full_name}]") + return RpcResult.failure(value=exception, message=str(exception)) + + result = None # Initialize result + + def callback_wrapper(payload): + nonlocal result + result = handle_response(payload) + + response_future.add_done_callback(callback_wrapper) + return result + + @staticmethod + def calculate_status_result(payload): + status = RpcMapper.unpack_payload(payload, UStatus) + return RpcResult.success(status) if status.code == UCode.OK else RpcResult.failure(status) + + @staticmethod + def unpack_payload(payload, expected_cls): + """ + Unpack a payload of type {@link Any} into an object of type T, which is what was packing into the {@link Any} + object.

+ @param payload:an {@link Any} message containing a type of expectedClazz. + @param expected_cls:The class name of the object packed into the {@link Any} + @return:Returns an object of type T and of the class name specified, that was packed into the {@link Any} + object. + """ + try: + value = expected_cls() + value.ParseFromString(payload.value) + # payload.Unpack(expected_cls) + return value + except Exception as e: + raise RuntimeError(f"{str(e)} [{UStatus.__name__}]") from e diff --git a/uprotocol/rpc/rpcresult.py b/uprotocol/rpc/rpcresult.py new file mode 100644 index 0000000..67a6582 --- /dev/null +++ b/uprotocol/rpc/rpcresult.py @@ -0,0 +1,171 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod +from typing import Callable, TypeVar, Union + +from uprotocol.proto.ustatus_pb2 import UCode +from uprotocol.proto.ustatus_pb2 import UStatus + +T = TypeVar('T') + + +class RpcResult(ABC): + """ + Wrapper class for RPC Stub calls. It contains a Success with the type of the RPC call, or a failure with the + UStatus returned by the failed call. + """ + + @abstractmethod + def isSuccess(self) -> bool: + pass + + @abstractmethod + def isFailure(self) -> bool: + pass + + @abstractmethod + def getOrElse(self, default_value: Callable[[], T]) -> T: + pass + + @abstractmethod + def map(self, f: Callable[[T], T]) -> 'RpcResult': + pass + + @abstractmethod + def flatMap(self, f: Callable[[T], 'RpcResult']) -> 'RpcResult': + pass + + @abstractmethod + def filter(self, f: Callable[[T], bool]) -> 'RpcResult': + pass + + @abstractmethod + def failureValue(self) -> UStatus: + pass + + @abstractmethod + def successValue(self) -> T: + pass + + @staticmethod + def success(value: T) -> 'RpcResult': + return Success(value) + + @staticmethod + def failure(value: Union[UStatus,'Failure', Exception,] = None, code: UCode = UCode.UNKNOWN, + message: str = '') -> 'RpcResult': + return Failure(value, code, message) + + @staticmethod + def flatten(result: 'RpcResult') -> 'RpcResult': + return result.flatMap(lambda x: x) + + +class Success(RpcResult): + + def __init__(self, value: T): + self.value = value + + def isSuccess(self) -> bool: + return True + + def isFailure(self) -> bool: + return False + + def getOrElse(self, default_value: Callable[[], T]) -> T: + return self.successValue() + + def map(self, f: Callable[[T], T]) -> RpcResult: + try: + + return self.success(f(self.successValue())) + except Exception as e: + return self.failure(e) + + def flatMap(self, f: Callable[[T], RpcResult]) -> RpcResult: + try: + return f(self.successValue()) + except Exception as e: + return self.failure(e) + + def filter(self, f: Callable[[T], bool]) -> RpcResult: + try: + return self if f(self.successValue()) else self.failure(code=UCode.FAILED_PRECONDITION, message="filtered out") + except Exception as e: + return self.failure(e) + + def failureValue(self) -> UStatus: + raise ValueError("Method failureValue() called on a Success instance") + + def successValue(self) -> T: + return self.value + + def __str__(self) -> str: + return f"Success({self.successValue()})" + + +class Failure(RpcResult): + + def __init__(self, value: Union[UStatus,'Failure', Exception, None] = None, code: UCode = UCode.UNKNOWN, message: str = ''): + if isinstance(value, UStatus): + self.value = value + elif isinstance(value, Exception): + self.value = UStatus(code=code, message=str(value)) + elif isinstance(value, Failure): + self.value = value.failureValue() + else: + self.value = UStatus(code=code, message=message) + + def isSuccess(self) -> bool: + return False + + def isFailure(self) -> bool: + return True + + def getOrElse(self, default_value: Callable[[], T]) -> T: + if callable(default_value): + return default_value() + return default_value + + def map(self, f: Callable[[T], T]) -> RpcResult: + return self.failure(self) + + def flatMap(self, f: Callable[[T], RpcResult]) -> RpcResult: + return self.failure(self.failureValue()) + + def filter(self, f: Callable[[T], bool]) -> RpcResult: + return self.failure(self) + + def failureValue(self) -> UStatus: + return self.value + + def successValue(self) -> T: + raise ValueError("Method successValue() called on a Failure instance") + + def __str__(self) -> str: + return f"Failure({self.value})" diff --git a/uprotocol/transport/README.adoc b/uprotocol/transport/README.adoc new file mode 100644 index 0000000..3f2c834 --- /dev/null +++ b/uprotocol/transport/README.adoc @@ -0,0 +1,7 @@ += uProtocol Transport Interface & Data Model +:toc: +:sectnums: +:source-highlighter: prettify + +== Overview +The purpose of this module is to provide the Python implementation of https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/up-l1/README.adoc[uTransport API & Data Model]. The transport API is used by all uE developers to send and receive messages across any transport. The interface is to be implemented by communication transport developers (i.e. developing a uTransport for SOME/IP, DDS, Zenoh, MQTT, etc...). diff --git a/uprotocol/transport/__init__.py b/uprotocol/transport/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/transport/builder/__init__.py b/uprotocol/transport/builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/transport/builder/uattributesbuilder.py b/uprotocol/transport/builder/uattributesbuilder.py new file mode 100644 index 0000000..5a55ee1 --- /dev/null +++ b/uprotocol/transport/builder/uattributesbuilder.py @@ -0,0 +1,197 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from uprotocol.proto.uattributes_pb2 import UAttributes, UPriority, UMessageType +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.uuid.factory.uuidfactory import * + + +class UAttributesBuilder: + """ + Construct the UAttributesBuilder with the configurations that are required for every payload transport. + + @param id Unique identifier for the message. + @param type Message type such as Publish a state change, RPC request or RPC response. + @param priority uProtocol Prioritization classifications. + """ + + def __init__(self, id: UUID, type: UMessageType, priority: UPriority): + self.id = id + self.type = UMessageType.Name(type) + self.priority = priority + self.ttl = None + self.token = None + self.sink = None + self.plevel = None + self.commstatus = None + self.reqid = None + + @staticmethod + def publish(priority: UPriority): + """ + Construct a UAttributesBuilder for a publish message. + @param priority The priority of the message. + @return Returns the UAttributesBuilder with the configured priority. + """ + if priority is None: + raise ValueError("UPriority cannot be None.") + return UAttributesBuilder(Factories.UPROTOCOL.create(), UMessageType.UMESSAGE_TYPE_PUBLISH, priority) + + @staticmethod + def notification(priority: UPriority, sink: UUri): + """ + Construct a UAttributesBuilder for a notification message. + @param priority The priority of the message. + @param sink The destination URI. + @return Returns the UAttributesBuilder with the configured priority and sink. + """ + if priority is None: + raise ValueError("UPriority cannot be null.") + if sink is None: + raise ValueError("sink cannot be null.") + return UAttributesBuilder(Factories.UPROTOCOL.create(), UMessageType.UMESSAGE_TYPE_PUBLISH, priority).withSink( + sink) + + @staticmethod + def request(priority: UPriority, sink: UUri, ttl: int): + """ + Construct a UAttributesBuilder for a request message. + @param priority The priority of the message. + @param sink The destination URI. + @param ttl The time to live in milliseconds. + @return Returns the UAttributesBuilder with the configured priority, sink and ttl. + """ + if priority is None: + raise ValueError("UPriority cannot be null.") + if sink is None: + raise ValueError("sink cannot be null.") + if ttl is None: + raise ValueError("ttl cannot be null.") + + return UAttributesBuilder(Factories.UPROTOCOL.create(), UMessageType.UMESSAGE_TYPE_REQUEST, priority).withTtl( + ttl).withSink(sink) + + @staticmethod + def response(priority: UPriority, sink: UUri, reqid: UUID): + """ + Construct a UAttributesBuilder for a response message. + @param priority The priority of the message. + @param sink The destination URI. + @param reqid The original request UUID used to correlate the response to the request. + @return Returns the UAttributesBuilder with the configured priority, sink and reqid. + """ + if priority is None: + raise ValueError("UPriority cannot be null.") + if sink is None: + raise ValueError("sink cannot be null.") + if reqid is None: + raise ValueError("reqid cannot be null.") + + return UAttributesBuilder(Factories.UPROTOCOL.create(), UMessageType.UMESSAGE_TYPE_RESPONSE, priority).withSink( + sink).withReqId(reqid) + + def withTtl(self, ttl: int): + """ + Add the time to live in milliseconds. + + @param ttl the time to live in milliseconds. + @return Returns the UAttributesBuilder with the configured ttl. + """ + self.ttl = ttl + return self + + def withToken(self, token: str): + """ + dd the authorization token used for TAP. + + @param token the authorization token used for TAP. + @return Returns the UAttributesBuilder with the configured token. + """ + self.token = token + return self + + def withSink(self, sink: UUri): + """ + Add the explicit destination URI. + + @param sink the explicit destination URI. + @return Returns the UAttributesBuilder with the configured sink. + """ + self.sink = sink + return self + + def withPermissionLevel(self, plevel: int): + """ + Add the permission level of the message. + + @param plevel the permission level of the message. + @return Returns the UAttributesBuilder with the configured plevel. + """ + self.plevel = plevel + return self + + def withCommStatus(self, commstatus: int): + """ + Add the communication status of the message. + + @param commstatus the communication status of the message. + @return Returns the UAttributesBuilder with the configured commstatus. + """ + self.commstatus = commstatus + return self + + def withReqId(self, reqid: UUID): + """ + Add the request ID. + + @param reqid the request ID. + @return Returns the UAttributesBuilder with the configured reqid. + """ + self.reqid = reqid + return self + + def build(self): + """ + Construct the UAttributes from the builder. + + @return Returns a constructed + """ + attributes = UAttributes(id=self.id, type=self.type, priority=self.priority) + if self.sink is not None: + attributes.sink.CopyFrom(self.sink) + if self.ttl is not None: + attributes.ttl = self.ttl + if self.plevel is not None: + attributes.permission_level = self.plevel + if self.commstatus is not None: + attributes.commstatus = self.commstatus + if self.reqid is not None: + attributes.reqid.CopyFrom(self.reqid) + if self.token != None: + attributes.token = self.token + return attributes diff --git a/uprotocol/transport/ulistener.py b/uprotocol/transport/ulistener.py new file mode 100644 index 0000000..1e69b69 --- /dev/null +++ b/uprotocol/transport/ulistener.py @@ -0,0 +1,51 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod + +from uprotocol.proto.uattributes_pb2 import UAttributes +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.proto.upayload_pb2 import UPayload +from uprotocol.proto.ustatus_pb2 import UStatus + + +class UListener(ABC): + """ + For any implementation that defines some kind of callback or function that will be called to handle incoming + messages. + """ + + @abstractmethod + def on_receive(self, topic: UUri, payload: UPayload, attributes: UAttributes) -> UStatus: + """ + Method called to handle/process events.

+ @param topic: Topic the underlying source of the message. + @param payload: Payload of the message. + @param attributes: Transportation attributes. + @return Returns an Ack every time a message is received and processed. + """ + pass diff --git a/uprotocol/transport/utransport.py b/uprotocol/transport/utransport.py new file mode 100644 index 0000000..eaad0c2 --- /dev/null +++ b/uprotocol/transport/utransport.py @@ -0,0 +1,88 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod + +from uprotocol.proto.uattributes_pb2 import UAttributes +from uprotocol.proto.uri_pb2 import UEntity +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.transport.ulistener import UListener +from uprotocol.proto.upayload_pb2 import UPayload +from uprotocol.proto.ustatus_pb2 import UStatus + +class UTransport(ABC): + """ + UTransport is the uP-L1 interface that provides a common API for uE developers to send and receive + messages.
UTransport implementations contain the details for connecting to the underlying transport technology + and sending UMessage using the configured technology.
For more information please refer to + link + """ + + @abstractmethod + def authenticate(self, u_entity: UEntity) -> UStatus: + """ + API used to authenticate with the underlining transport layer that the uEntity passed matches the transport + specific identity. MUST pass a resolved UUri.

+ @param u_entity:Resolved UEntity + @return: Returns OKSTATUS if authenticate was successful, FAILSTATUS if the calling uE is not authenticated. + """ + pass + + @abstractmethod + def send(self, topic: UUri, payload: UPayload, attributes: UAttributes) -> UStatus: + """ + Transmit UPayload to the topic using the attributes defined in UTransportAttributes.

+ @param topic:Resolved UUri topic to send the payload to. + @param payload:Actual payload. + @param attributes:Additional transport attributes. + @return:Returns OKSTATUS if the payload has been successfully sent (ACK'ed), otherwise it returns FAILSTATUS + with the appropriate failure. + """ + pass + + @abstractmethod + def register_listener(self, topic: UUri, listener: UListener) -> UStatus: + """ + Register listener to be called when UPayload is received for the specific topic.

+ @param topic:Resolved UUri for where the message arrived via the underlying transport technology. + @param listener:The method to execute to process the date for the topic. + @return:Returns OKSTATUS if the listener is unregistered correctly, otherwise it returns FAILSTATUS with the + appropriate failure. + """ + pass + + @abstractmethod + def unregister_listener(self, topic: UUri, listener: UListener) -> UStatus: + """ + Unregister a listener for a given topic. Messages arriving on this topic will no longer be processed by this + listener. + @param topic:Resolved UUri for where the listener was registered to receive messages from. + @param listener:The method to execute to process the date for the topic. + @return:Returns OKSTATUS if the listener is unregistered correctly, otherwise it returns FAILSTATUS with the + appropriate failure. + """ + pass diff --git a/uprotocol/transport/validate/__init__.py b/uprotocol/transport/validate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/transport/validate/uattributesvalidator.py b/uprotocol/transport/validate/uattributesvalidator.py new file mode 100644 index 0000000..3017fb0 --- /dev/null +++ b/uprotocol/transport/validate/uattributesvalidator.py @@ -0,0 +1,295 @@ +# ------------------------------------------------------------------------- +import time +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import abstractmethod +from datetime import datetime +from enum import Enum + +from uprotocol.proto.uattributes_pb2 import UAttributes, UMessageType +from uprotocol.proto.ustatus_pb2 import UCode +from uprotocol.uri.validator.urivalidator import UriValidator +from uprotocol.uuid.factory.uuidutils import UUIDUtils +from uprotocol.validation.validationresult import ValidationResult + + +class UAttributesValidator: + """ + UAttributes is the class that defines the Payload. It is the place for configuring time to live, priority, + security tokens and more.

+ Each UAttributes class defines a different type of message payload. The payload can represent a simple published + payload with some state change,Payload representing an RPC request or Payload representing an RPC response.

+ UAttributesValidator is a base class for all UAttribute validators, that can help validate that the + UAttributes object is correctly defined to define the Payload correctly. + + """ + + @staticmethod + def get_validator(attribute: UAttributes): + """ + Static factory method for getting a validator according to the UMessageType defined in the + UAttributes.
+ @param attribute: UAttributes containing the UMessageType. + @return: returns a UAttributesValidator according to the UMessageType defined in the + UAttributes. + """ + if attribute.type is None: + return Validators.PUBLISH.validator() + elif attribute.type == UMessageType.UMESSAGE_TYPE_RESPONSE: + return Validators.RESPONSE.validator() + elif attribute.type == UMessageType.UMESSAGE_TYPE_REQUEST: + return Validators.REQUEST.validator() + else: + return Validators.PUBLISH.validator() + + def validate(self, attributes: UAttributes) -> ValidationResult: + """ + Take a UAttributes object and run validations.

+ @param attributes:The UAttriubes to validate. + @return:Returns a ValidationResult that is success or failed with a message containing all validation errors + for invalid configurations. + """ + error_messages = [self.validate_type(attributes), + self.validate_ttl(attributes), + self.validate_sink(attributes), self.validate_comm_status(attributes), + self.validate_permission_level(attributes), self.validate_req_id(attributes)] + + error_messages = [status.get_message() for status in error_messages if + status.is_failure()] + + if error_messages: + return ValidationResult.failure(",".join(error_messages)) + else: + return ValidationResult.success() + + @staticmethod + def is_expired(u_attributes: UAttributes): + """ + Indication if the Payload with these UAttributes is expired.

+ @param u_attributes:UAttributes with time to live value. + @return: Returns a ValidationResult that is success meaning not expired or failed with a validation message or + expiration. + """ + maybe_ttl = u_attributes.ttl + maybe_time = UUIDUtils.getTime(u_attributes.id) + + ttl = maybe_ttl + if ttl <= 0: + return ValidationResult.success() + delta = int(time.time() * 1000)- maybe_time + return ValidationResult.failure("Payload is expired") if delta >= ttl else ValidationResult.success() + + @staticmethod + def validate_ttl(attr: UAttributes) -> ValidationResult: + """ + Validate the time to live configuration. If the UAttributes does not contain a time to live then the + ValidationResult + is ok.

+ @param attr:UAttributes object containing the message time to live configuration to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + if attr.HasField('ttl') and attr.ttl <= 0: + return ValidationResult.failure(f"Invalid TTL [{attr.ttl}]") + else: + return ValidationResult.success() + + @staticmethod + def validate_sink(attr: UAttributes) -> ValidationResult: + """ + Validate the sink UriPart for the default case. If the UAttributes does not contain a sink then the + ValidationResult + is ok.

+ @param attr:UAttributes object containing the sink to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + return UriValidator.validate(attr.sink) if attr.HasField('sink') else ValidationResult.success() + + @staticmethod + def validate_permission_level(attr: UAttributes) -> ValidationResult: + """ + Validate the permissionLevel for the default case. If the UAttributes does not contain a permission level + then the ValidationResult is ok.

+ @param attr:UAttributes object containing the permission level to validate. + @return:Returns a ValidationResult indicating if the permissionLevel is valid or not. + """ + if attr.HasField('permission_level') and attr.permission_level <= 0: + return ValidationResult.failure("Invalid Permission Level") + else: + return ValidationResult.success() + + @staticmethod + def validate_comm_status(attr: UAttributes) -> ValidationResult: + """ + Validate the commStatus for the default case. If the UAttributes does not contain a comm status then the + ValidationResult is ok.

+ @param attr:UAttributes object containing the comm status to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + if attr.HasField('commstatus'): + try: + UCode.Name(attr.commstatus) + except ValueError: + return ValidationResult.failure("Invalid Communication Status Code") + + return ValidationResult.success() + + @staticmethod + def validate_req_id(attr: UAttributes) -> ValidationResult: + """ + Validate the correlationId for the default case. If the UAttributes does not contain a request id then the + ValidationResult is ok.

+ @param attr:Attributes object containing the request id to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + + if attr.HasField('reqid') and not UUIDUtils.isuuid(attr.reqid): + return ValidationResult.failure("Invalid UUID") + else: + return ValidationResult.success() + + @abstractmethod + def validate_type(self, attr: UAttributes): + """ + Validate the UMessageType attribute, it is required.

+ @param attr:UAttributes object containing the message type to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + raise NotImplementedError("Subclasses must implement this method.") + + +class Publish(UAttributesValidator): + """ + Implements validations for UAttributes that define a message that is meant for publishing state changes. + """ + + def validate_type(self, attributes_value: UAttributes) -> ValidationResult: + """ + Validates that attributes for a message meant to publish state changes has the correct type.

+ @param attributes_value:UAttributes object containing the message type to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + return ValidationResult.success() if attributes_value.type == UMessageType.UMESSAGE_TYPE_PUBLISH else ( + ValidationResult.failure( + f"Wrong Attribute Type [{UMessageType.Name(attributes_value.type)}]")) + + def __str__(self): + return "UAttributesValidator.Publish" + + +class Request(UAttributesValidator): + """ + Implements validations for UAttributes that define a message that is meant for an RPC request. + """ + + def validate_type(self, attributes_value: UAttributes) -> ValidationResult: + """ + Validates that attributes for a message meant for an RPC request has the correct type.

+ @param attributes_value:UAttributes object containing the message type to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + return ValidationResult.success() if attributes_value.type == UMessageType.UMESSAGE_TYPE_REQUEST else ( + ValidationResult.failure( + f"Wrong Attribute Type [{UMessageType.Name(attributes_value.type)}]")) + + def validate_sink(self, attributes_value: UAttributes) -> ValidationResult: + """ + Validates that attributes for a message meant for an RPC request has a destination sink.

In the case + of an RPC request, the sink is required. + @param attributes_value:UAttributes object containing the sink to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + return UriValidator.validate_rpc_response( + attributes_value.sink) if attributes_value.HasField('sink') else ValidationResult.failure("Missing Sink") + + def validate_ttl(self, attributes_value: UAttributes) -> ValidationResult: + """ + Validate the time to live configuration.
In the case of an RPC request, the time to live is required.

+ @param attributes_value:UAttributes object containing the time to live to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + if not attributes_value.HasField('ttl'): + return ValidationResult.failure("Missing TTL") + if attributes_value.ttl <= 0: + return ValidationResult.failure(f"Invalid TTL [{attributes_value.ttl}]") + + return ValidationResult.success() + + def __str__(self): + return "UAttributesValidator.Request" + + +class Response(UAttributesValidator): + """ + Implements validations for UAttributes that define a message that is meant for an RPC response. + """ + + def validate_type(self, attributes_value: UAttributes) -> ValidationResult: + """ + Validates that attributes for a message meant for an RPC response has the correct type.

+ @param attributes_value:UAttributes object containing the message type to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + return ValidationResult.success() if attributes_value.type == UMessageType.UMESSAGE_TYPE_RESPONSE else ( + ValidationResult.failure( + f"Wrong Attribute Type [{UMessageType.Name(attributes_value.type)}]")) + + def validate_sink(self, attributes_value: UAttributes) -> ValidationResult: + """ + Validates that attributes for a message meant for an RPC response has a destination sink.
In the case of + an RPC response, the sink is required.

+ @param attributes_value:UAttributes object containing the sink to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + result = UriValidator.validate_rpc_method(attributes_value.sink) + if result.is_success(): + return result + else: + return ValidationResult.failure("Missing Sink") + + def validate_req_id(self, attributes_value: UAttributes) -> ValidationResult: + """ + Validate the correlationId. n the case of an RPC response, the correlation id is required.

+ @param attributes_value:UAttributes object containing the correlation id to validate. + @return:Returns a ValidationResult that is success or failed with a failure message. + """ + return ValidationResult.success() if attributes_value.reqid and UUIDUtils.isuuid( + attributes_value.reqid) else ValidationResult.failure("Missing correlationId") + + def __str__(self): + return "UAttributesValidator.Response" + + +class Validators(Enum): + """ + Validators Factory.
Example: UAttributesValidator validateForPublishMessageType = + UAttributesValidator.Validators.PUBLISH.validator() + """ + PUBLISH = Publish() + REQUEST = Request() + RESPONSE = Response() + + def validator(self): + return self.value diff --git a/uprotocol/uri/README.adoc b/uprotocol/uri/README.adoc new file mode 100644 index 0000000..579a6f9 --- /dev/null +++ b/uprotocol/uri/README.adoc @@ -0,0 +1,45 @@ += uProtocol URI (UUri) +:toc: +:sectnums: + + +== Overview + +The following folder contains everything but the data model for UUri (builders, serializers, validators, etc...) per https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uri.adoc[uProtocol URI Specifications]. +The data model is defined in https://github.com/eclipse-uprotocol/uprotocol-core-api/blob/main/src/main/proto/uri.proto[uri.proto] and included as a dependency for this project. + +IMPORTANT: For more details about the data model, various formats (object, long, micro) and their uses, please refer to https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uri.adoc[uProtocol URI Specifications]. + + +== Using the SDK + +When building UUri, you can choose to populate it with only names, only numbers, or both (resolved). When you should use each is described the best practice section of https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uri.adoc[uProtocol URI Specifications]. + +=== Building an RPC Method +[,python] +---- +uri: UUri = UUri( + authority=UAuthority(name="MyDevice", ip=bytes(socket.inet_pton(socket.AF_INET, "192.168.1.100")), + id=bytes.fromhex("3GTU2NEC8HG403825")), + entity=UEntity(name="HartleyService", id=10203, version_major=1), + resource=UResourceBuilder.for_rpc_request("Raise", 10)) +---- + +=== Validating +[,python] +---- +status : ValidationResult = UriValidator.validate_rpc_method(uuri) +assertTrue(status.is_success()); +---- + +=== Serializing & Deserializing +[,python] +---- + uri = .../* UUri example above */ + micro = MicroUriSerializer().serialize(uri) + long = LongUriSerializer().serialize(uri) + deserialized_micro_uuri = MicroUriSerializer().deserialize(uri) + deserialized_long_uuri = LongUriSerializer().deserialize(uri) + uri2 = UriSerializer.build_resolved(long, micro) + +---- \ No newline at end of file diff --git a/uprotocol/uri/__init__.py b/uprotocol/uri/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/uri/builder/__init__.py b/uprotocol/uri/builder/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/uri/builder/uresource_builder.py b/uprotocol/uri/builder/uresource_builder.py new file mode 100644 index 0000000..6df1be5 --- /dev/null +++ b/uprotocol/uri/builder/uresource_builder.py @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from uprotocol.proto.uri_pb2 import UResource + + +class UResourceBuilder: + MAX_RPC_ID = 1000 + + @staticmethod + def for_rpc_response(): + return UResource(name="rpc", instance="response", id=0) + + @staticmethod + def for_rpc_request(method, id=None): + uresource = UResource(name="rpc") + if method is not None: + uresource.instance = method + if id is not None: + uresource.id = id + + return uresource + + @staticmethod + def for_rpc_request_with_id(id): + return UResourceBuilder.for_rpc_request(None, id) + + @staticmethod + def from_id(id): + if id is None: + raise ValueError("id cannot be None") + + return UResourceBuilder.for_rpc_request_with_id(id) if id < UResourceBuilder.MAX_RPC_ID else UResource(id=id) diff --git a/uprotocol/uri/serializer/__init__.py b/uprotocol/uri/serializer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/uri/serializer/longuriserializer.py b/uprotocol/uri/serializer/longuriserializer.py new file mode 100644 index 0000000..a0afe26 --- /dev/null +++ b/uprotocol/uri/serializer/longuriserializer.py @@ -0,0 +1,211 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import re + +from uprotocol.proto.uri_pb2 import UAuthority +from uprotocol.proto.uri_pb2 import UEntity +from uprotocol.proto.uri_pb2 import UResource +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.uri.serializer.uriserializer import UriSerializer +from uprotocol.uri.validator.urivalidator import UriValidator + + +class LongUriSerializer(UriSerializer): + """ + UUri Serializer that serializes a UUri to a long format string per + + https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uri.adoc + """ + + def serialize(self, uri: UUri) -> str: + """ + Support for serializing {@link UUri} objects into their String format.

+ @param uri: UUri object to be serialized to the String format. + @return:Returns the String format of the supplied UUri that can be used as a sink or a source in a + uProtocol publish communication. + """ + if uri is None or UriValidator.is_empty(uri): + return "" + + sb = [] + + if uri.HasField('authority'): + sb.append(self.build_authority_part_of_uri(uri.authority)) + + sb.append("/") + + sb.append(self.build_software_entity_part_of_uri(uri.entity)) + sb.append(self.build_resource_part_of_uri(uri)) + + return re.sub('/+$', '', "".join(sb)) + + @staticmethod + def build_resource_part_of_uri(uuri: UUri) -> str: + + if not uuri.HasField('resource'): + return "" + u_resource = uuri.resource + + sb = "/" + u_resource.name + + if u_resource.instance: + sb += "." + u_resource.instance + if u_resource.message: + sb += "#" + u_resource.message + + return sb + + @staticmethod + def build_software_entity_part_of_uri(entity: UEntity) -> str: + """ + Create the service part of the uProtocol URI from an UEntity object.

+ @param entity:Software Entity representing a service or an application. + @return: Returns the String representation of the UEntity in the uProtocol URI. + """ + sb = str(entity.name.strip()) + sb += "/" + + if entity.version_major > 0: + sb += str(entity.version_major) + + return sb + + @staticmethod + def build_authority_part_of_uri(authority: UAuthority) -> str: + """ + Create the authority part of the uProtocol URI from an UAuthority object.

+ @param authority:represents the deployment location of a specific Software Entity in the Ultiverse. + @return:Returns the String representation of the Authority in the uProtocol URI. + """ + + partial_uri = "//" + maybe_name = authority.name + + if maybe_name is not None or maybe_name != "": + partial_uri += maybe_name + + return partial_uri + + def deserialize(self, u_protocol_uri: str) -> UUri: + """ + Deserialize a String into a UUri object.

+ @param u_protocol_uri:A long format uProtocol URI. + @return:Returns an UUri data object. + """ + if u_protocol_uri is None or u_protocol_uri.strip() == "": + return UUri() + uri = u_protocol_uri[u_protocol_uri.index(":") + 1:] \ + if ":" in u_protocol_uri else u_protocol_uri.replace('\\', '/') + + is_local = not uri.startswith("//") + uri_parts = LongUriSerializer.remove_empty(uri.split("/")) + number_of_parts_in_uri = len(uri_parts) + + if number_of_parts_in_uri == 0 or number_of_parts_in_uri == 1: + return UUri() + + use_name = "" + use_version = "" + u_resource = None + u_authority = None + + if is_local: + use_name = uri_parts[1] + if number_of_parts_in_uri > 2: + use_version = uri_parts[2] + if number_of_parts_in_uri > 3: + u_resource = self.parse_from_string(uri_parts[3]) + + else: + if uri_parts[2].strip() == "": + return UUri() + u_authority = UAuthority(name=uri_parts[2]) + if len(uri_parts) > 3: + use_name = uri_parts[3] + if number_of_parts_in_uri > 4: + use_version = uri_parts[4] + if number_of_parts_in_uri > 5: + u_resource = self.parse_from_string(uri_parts[5]) + else: + return UUri(authority=u_authority) + + use_version_int = None + try: + if use_version.strip() != "": + use_version_int = int(use_version) + except ValueError: + return UUri() + + u_entity_builder = UEntity(name=use_name) + if use_version_int is not None: + u_entity_builder.version_major = use_version_int + + new_uri = UUri(entity=u_entity_builder) + if u_authority is not None: + new_uri.authority.CopyFrom(u_authority) + + if u_resource is not None: + new_uri.resource.CopyFrom(u_resource) + + return new_uri + + @staticmethod + def parse_from_string(resource_string: str) -> UResource: + """ + Static builder method for creating a UResource using a string that contains name + instance + message.

+ @param resource_string:String that contains the UResource information. + @return:Returns a UResource object. + """ + if resource_string is None or resource_string.strip() == "": + raise ValueError("Resource must have a command name.") + + parts = LongUriSerializer.remove_empty(resource_string.split("#")) + name_and_instance = parts[0] + + name_and_instance_parts = LongUriSerializer.remove_empty(name_and_instance.split(".")) + resource_name = name_and_instance_parts[0] + resource_instance = name_and_instance_parts[1] if len(name_and_instance_parts) > 1 else None + resource_message = parts[1] if len(parts) > 1 else None + + u_resource = UResource(name=resource_name) + if resource_instance is not None: + u_resource.instance = resource_instance + if resource_message is not None: + u_resource.message = resource_message + + return u_resource + + @staticmethod + def remove_empty(parts): + result = parts[:] + + # Iterate through the list in reverse and remove empty strings + while result and result[-1] == '': + result.pop() + + return result diff --git a/uprotocol/uri/serializer/microuriserializer.py b/uprotocol/uri/serializer/microuriserializer.py new file mode 100644 index 0000000..80ab4c6 --- /dev/null +++ b/uprotocol/uri/serializer/microuriserializer.py @@ -0,0 +1,195 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import io +import struct +from enum import Enum + +from uprotocol.proto.uri_pb2 import UAuthority +from uprotocol.proto.uri_pb2 import UEntity +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.uri.builder.uresource_builder import UResourceBuilder +from uprotocol.uri.serializer.uriserializer import UriSerializer +from uprotocol.uri.validator.urivalidator import UriValidator + + +class AddressType(Enum): + """ + The type of address used for Micro URI. + """ + LOCAL = 0 + IPv4 = 1 + IPv6 = 2 + ID = 3 + + def getValue(self): + return bytes(self.value) + + @classmethod + def from_value(cls, value): + for addr_type in cls: + if addr_type.value == value: + return addr_type + return None # Return None if no matching enum value is found + + +class MicroUriSerializer(UriSerializer): + """ + UUri Serializer that serializes a UUri to a byte[] (micro format) per + https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uri.adoc + """ + LOCAL_MICRO_URI_LENGTH = 8 + IPV4_MICRO_URI_LENGTH = 12 + IPV6_MICRO_URI_LENGTH = 24 + UP_VERSION = 0x1 + + def serialize(self, uri: UUri) -> bytes: + """ + Serialize a UUri into a byte[] following the Micro-URI specifications.

+ @param uri:The UUri data object. + @return:Returns a byte[] representing the serialized UUri. + """ + + if uri is None or UriValidator.is_empty(uri) or not UriValidator.is_micro_form(uri): + return bytearray() + + maybe_ue_id = uri.entity.id + maybe_uresource_id = uri.resource.id + + os = io.BytesIO() + os.write(bytes([self.UP_VERSION])) + + # Determine the uAuthority type to be written + remote_case = "REMOTE_NOT_SET" + + if len(uri.authority.ip) > 0: + remote_case = "IP" + elif len(uri.authority.id) > 0: + remote_case = "ID" + elif len(uri.authority.name) > 0: + remote_case = "NAME" + if remote_case == "REMOTE_NOT_SET": + address_type = AddressType.LOCAL + elif remote_case == "IP": + length = len(uri.authority.ip) + if length == 4: + address_type = AddressType.IPv4 + elif length == 16: + address_type = AddressType.IPv6 + else: + return bytearray() + elif remote_case == "ID": + address_type = AddressType.ID + else: + return bytearray() + + os.write(address_type.value.to_bytes(1, 'big')) + + # URESOURCE_ID + os.write((maybe_uresource_id >> 8).to_bytes(1, 'big')) + os.write((maybe_uresource_id & 0xFF).to_bytes(1, 'big')) + + # UENTITY_ID + os.write((maybe_ue_id >> 8).to_bytes(1, 'big')) + os.write((maybe_ue_id & 0xFF).to_bytes(1, 'big')) + + # UE_VERSION + unsigned_value = uri.entity.version_major + if unsigned_value > 127: + signed_byte = unsigned_value - 256 + else: + signed_byte = unsigned_value + os.write(struct.pack('b', signed_byte)) + # UNUSED + os.write(bytes([0])) + + # Populating the UAuthority + if address_type != AddressType.LOCAL: + # Write the ID length if the type is ID + if address_type == AddressType.ID: + os.write(len(uri.authority.id).to_bytes(1, 'big')) + + try: + if remote_case == "IP": + os.write(uri.authority.ip) + elif remote_case == "ID": + os.write(uri.authority.id) + except Exception as e: + print(e) # Handle the exception as needed + + return os.getvalue() + + def deserialize(self, micro_uri: bytes) -> UUri: + """ + Deserialize a byte[] into a UUri object.

+ @param micro_uri:A byte[] uProtocol micro URI. + @return:Returns an UUri data object from the serialized format of a microUri. + """ + if micro_uri is None or len(micro_uri) < self.LOCAL_MICRO_URI_LENGTH: + return UUri() + + if micro_uri[0] != self.UP_VERSION: + return UUri() + + u_resource_id = ((micro_uri[2] & 0xFF) << 8) | (micro_uri[3] & 0xFF) + addresstype = AddressType.from_value(micro_uri[1]) + + # Validate Type is found + if addresstype is None: + return UUri() + + # Validate that the micro_uri is the correct length for the type + address_type = addresstype + if address_type == AddressType.LOCAL and len(micro_uri) != self.LOCAL_MICRO_URI_LENGTH: + return UUri() + elif address_type == AddressType.IPv4 and len(micro_uri) != self.IPV4_MICRO_URI_LENGTH: + return UUri() + elif address_type == AddressType.IPv6 and len(micro_uri) != self.IPV6_MICRO_URI_LENGTH: + return UUri() + + # UENTITY_ID + ue_id = ((micro_uri[4] & 0xFF) << 8) | (micro_uri[5] & 0xFF) + + # UE_VERSION + ui_version = micro_uri[6] + + u_authority = None + if address_type in (AddressType.IPv4, AddressType.IPv6): + length = 4 if address_type == AddressType.IPv4 else 16 + data = micro_uri[8:8 + length] + u_authority = UAuthority(ip=bytes(data)) + elif address_type == AddressType.ID: + length = micro_uri[8] + u_authority = UAuthority(id=bytes(micro_uri[9:9 + length])) + + uri = UUri(entity=UEntity(id=ue_id, version_major=ui_version), resource=UResourceBuilder.from_id(u_resource_id)) + + if u_authority is not None: + uri.authority.CopyFrom(u_authority) + + return uri diff --git a/uprotocol/uri/serializer/uriserializer.py b/uprotocol/uri/serializer/uriserializer.py new file mode 100644 index 0000000..49c527e --- /dev/null +++ b/uprotocol/uri/serializer/uriserializer.py @@ -0,0 +1,89 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod +from re import T +from typing import Optional +from uprotocol.proto.uri_pb2 import UUri,UAuthority,UEntity,UResource +from uprotocol.uri.validator.urivalidator import UriValidator + + +class UriSerializer(ABC): + """ + UUris are used in transport layers and hence need to be serialized.
Each transport supports different + serialization formats.
For more information, please refer to + https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uri.adoc + """ + + @abstractmethod + def deserialize(self, uri: T) -> UUri: + """ + Deserialize from the format to a UUri.

+ @param uri:serialized UUri. + @return:Returns a UUri object from the serialized format from the wire. + """ + pass + + @abstractmethod + def serialize(self, uri: UUri) -> T: + """ + Serialize from a UUri to a specific serialization format.

+ @param uri:UUri object to be serialized to the format T. + @return:Returns the UUri in the transport serialized format. + """ + pass + + def build_resolved(self, long_uri: str, micro_uri: bytes) -> UUri: + """ + Build a fully resolved {@link UUri} from the serialized long format and the serializes micro format.

+ @param long_uri:UUri serialized as a Sting. + @param micro_uri:UUri serialized as a byte[]. + @return:Returns a UUri object serialized from one of the forms. + """ + if (not long_uri or long_uri.isspace()) and (not micro_uri or len(micro_uri) == 0): + return UUri() + from uprotocol.uri.serializer.longuriserializer import LongUriSerializer + from uprotocol.uri.serializer.microuriserializer import MicroUriSerializer + long_u_uri = LongUriSerializer().deserialize(long_uri) + micro_u_uri = MicroUriSerializer().deserialize(micro_uri) + u_authority = UAuthority() + u_authority.CopyFrom(micro_u_uri.authority) + + u_authority.name = long_u_uri.authority.name + + u_entity = UEntity() + u_entity.CopyFrom(micro_u_uri.entity) + + u_entity.name = long_u_uri.entity.name + + u_resource = UResource() + u_resource.CopyFrom(long_u_uri.resource) + u_resource.id = micro_u_uri.resource.id + + u_uri = UUri(authority=u_authority, entity=u_entity, resource=u_resource) + return u_uri if UriValidator.is_resolved(u_uri) else None diff --git a/uprotocol/uri/validator/__init__.py b/uprotocol/uri/validator/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/uri/validator/urivalidator.py b/uprotocol/uri/validator/urivalidator.py new file mode 100644 index 0000000..ca3a7f8 --- /dev/null +++ b/uprotocol/uri/validator/urivalidator.py @@ -0,0 +1,157 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from uprotocol.proto.uri_pb2 import UAuthority +from uprotocol.proto.uri_pb2 import UEntity +from uprotocol.proto.uri_pb2 import UResource +from uprotocol.proto.uri_pb2 import UUri +from uprotocol.validation.validationresult import ValidationResult + + +class UriValidator: + """ + Class for validating Uris. + """ + + @staticmethod + def validate(uri: UUri) -> ValidationResult: + """ + Validate a UUri to ensure that it has at least a name for the uEntity.

+ @param uri:UUri to validate. + @return:Returns UStatus containing a success or a failure with the error message. + """ + if UriValidator.is_empty(uri): + return ValidationResult.failure("Uri is empty.") + + if uri.HasField('authority') and not UriValidator.is_remote(uri.authority): + return ValidationResult.failure("Uri is remote missing uAuthority.") + + if uri.entity.name.strip() == "": + return ValidationResult.failure("Uri is missing uSoftware Entity name.") + + return ValidationResult.success() + + @staticmethod + def validate_rpc_method(uri: UUri) -> ValidationResult: + """ + Validate a UUri that is meant to be used as an RPC method URI. Used in Request sink values and Response + source values.

+ @param uri:UUri to validate. + @return:Returns UStatus containing a success or a failure with the error message. + """ + status = UriValidator.validate(uri) + if status.is_failure(): + return status + + if not UriValidator.is_rpc_method(uri): + return ValidationResult.failure( + "Invalid RPC method uri. Uri should be the method to be called, or method from response.") + + return ValidationResult.success() + + @staticmethod + def validate_rpc_response(uri: UUri) -> ValidationResult: + """ + Validate a UUri that is meant to be used as an RPC response URI. Used in Request source values and + Response sink values.

+ @param uri:UUri to validate. + @return:Returns UStatus containing a success or a failure with the error message. + """ + status = UriValidator.validate(uri) + if status.is_failure(): + return status + + if not UriValidator.is_rpc_response(uri): + return ValidationResult.failure("Invalid RPC response type.") + + return ValidationResult.success() + + @staticmethod + def is_empty(uri: UUri) -> bool: + if uri is None: + raise ValueError("Uri cannot be None.") + return not uri.HasField('authority') and not uri.HasField('entity') and not uri.HasField('resource') + + + @staticmethod + def is_rpc_method(uri: UUri) -> bool: + """ + Returns true if this resource specifies an RPC method call or RPC response.

+ @param uri: + @return:Returns true if this resource specifies an RPC method call or RPC response. + """ + if uri is None: + raise ValueError("Uri cannot be None.") + return not UriValidator.is_empty(uri) and uri.resource.name == "rpc" and ( + uri.resource.HasField('instance') and uri.resource.instance.strip() != "" or ( + uri.resource.HasField('id') and uri.resource.id != 0)) + + @staticmethod + def is_resolved(uuri: UUri) -> bool: + if uuri is None: + raise ValueError("Uri cannot be None.") + + return not UriValidator.is_empty(uuri) + + @staticmethod + def is_rpc_response(uuri: UUri) -> bool: + if uuri is None: + raise ValueError("Uri cannot be None.") + + return UriValidator.is_rpc_method(uuri) and ( + (uuri.resource.HasField('instance') and "response" in uuri.resource.instance) or ( + uuri.resource.HasField('id') and uuri.resource.id != 0)) + + @staticmethod + def is_micro_form(uuri: UUri) -> bool: + """ + Determines if this UUri can be serialized into a micro form UUri.

+ @param uuri: An UUri proto message object + @return:Returns true if this UUri can be serialized into a micro form UUri. + """ + if uuri is None: + raise ValueError("Uri cannot be None.") + return not UriValidator.is_empty(uuri) and uuri.entity.HasField('id') and uuri.resource.HasField('id') and ( + not uuri.HasField('authority') or uuri.authority.HasField('ip') or uuri.authority.HasField('id')) + + @staticmethod + def is_long_form(uuri: UUri) -> bool: + """ + Determines if this UUri can be serialized into a long form UUri.

+ @param uuri: An UUri proto message object + @return:Returns true if this UUri can be serialized into a long form UUri. + """ + if uuri is None: + raise ValueError("Uri cannot be None.") + return not UriValidator.is_empty(uuri) and not (uuri.HasField('authority') and uuri.authority.HasField( + 'name')) and not uuri.entity.name.strip() == '' and not uuri.resource.name.strip() == '' + + @staticmethod + def is_remote(authority: UAuthority) -> bool: + if authority is None: + raise ValueError("Authority cannot be None.") + return not all([authority.name.strip() == "", len(authority.ip) == 0, len(authority.id) == 0]) diff --git a/uprotocol/uuid/README.adoc b/uprotocol/uuid/README.adoc new file mode 100644 index 0000000..a598900 --- /dev/null +++ b/uprotocol/uuid/README.adoc @@ -0,0 +1,33 @@ += uProtocol UUID +:toc: +:sectnums: + +== Overview + +Implementation of https://github.com/eclipse-uprotocol/uprotocol-spec/blob/main/basics/uuid.adoc[uProtocol UUID specifications]. + +== Examples + +[source,python] +---- + uuid = Factories.UPROTOCOL.create() + version = UUIDUtils.getVersion(uuid) + time = UUIDUtils.getTime(uuid) + bytes_uuid = MicroUuidSerializer.instance().serialize(uuid) + str_uuid = LongUuidSerializer.instance().serialize(uuid) + + assertTrue(UUIDUtils.isUProtocol(uuid)) + assertTrue(UUIDUtils.isuuid(uuid)) + assertFalse(UUIDUtils.isUuidv6(uuid)) + assertTrue(version) + assertTrue(time) + assertGreater(len(bytes_uuid), 0) + assertFalse(str_uuid.isspace()) + + uuid1 = MicroUuidSerializer.instance().deserialize(bytes_data) + uuid2 = LongUuidSerializer.instance().deserialize(uuid_string) + assertNotEqual(uuid1, UUID()) + assertNotEqual(uuid2, UUID()) + assertEqual(uuid, uuid1) + assertEqual(uuid, uuid2) +---- \ No newline at end of file diff --git a/uprotocol/uuid/__init__.py b/uprotocol/uuid/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/uuid/factory/__init__.py b/uprotocol/uuid/factory/__init__.py new file mode 100644 index 0000000..ff4a2a0 --- /dev/null +++ b/uprotocol/uuid/factory/__init__.py @@ -0,0 +1,150 @@ + +import secrets +import time +import uuid +from typing import Tuple + + +class PythonUUID(uuid.UUID): + r"""UUID draft version objects""" + + def __init__( + self, + hex: str = None, + bytes: bytes = None, + bytes_le: bytes = None, + fields: Tuple[int, int, int, int, int, int] = None, + int: int = None, + version: int = None, + *, + is_safe=uuid.SafeUUID.unknown + ) -> None: + r"""Create a UUID.""" + + if int is None or [hex, bytes, bytes_le, fields].count(None) != 4: + return super().__init__( + hex=hex, + bytes=bytes, + bytes_le=bytes_le, + fields=fields, + int=int, + version=version, + is_safe=is_safe, + ) + if not 0 <= int < 1 << 128: + raise ValueError("int is out of range (need a 128-bit value)") + if version is not None: + if not 6 <= version <= 8: + raise ValueError("illegal version number") + # Set the variant to RFC 4122. + int &= ~(0xC000 << 48) + int |= 0x8000 << 48 + # Set the version number. + int &= ~(0xF000 << 64) + int |= version << 76 + super().__init__(int=int, is_safe=is_safe) + + @property + def subsec(self) -> int: + return ((self.int >> 64) & 0x0FFF) << 8 | ((self.int >> 54) & 0xFF) + + @property + def time(self) -> int: + if self.version == 6: + return ( + (self.time_low << 28) + | (self.time_mid << 12) + | (self.time_hi_version & 0x0FFF) + ) + if self.version == 7: + return self.int >> 80 + if self.version == 8: + return (self.int >> 80) * 10**6 + _subsec_decode(self.subsec) + return super().time + + +def _subsec_decode(value: int) -> int: + return -(-value * 10**6 // 2**20) + + +def _subsec_encode(value: int) -> int: + return value * 2**20 // 10**6 + + +_last_v6_timestamp = None +_last_v7_timestamp = None +_last_v8_timestamp = None + + +def uuid6(clock_seq: int = None) -> PythonUUID: + r"""UUID version 6 is a field-compatible version of UUIDv1, reordered for + improved DB locality. It is expected that UUIDv6 will primarily be + used in contexts where there are existing v1 UUIDs. Systems that do + not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead. + + If 'clock_seq' is given, it is used as the sequence number; + otherwise a random 14-bit sequence number is chosen.""" + + global _last_v6_timestamp + + nanoseconds = time.time_ns() + # 0x01b21dd213814000 is the number of 100-ns intervals between the + # UUID epoch 1582-10-15 00:00:00 and the Unix epoch 1970-01-01 00:00:00. + timestamp = nanoseconds // 100 + 0x01B21DD213814000 + if _last_v6_timestamp is not None and timestamp <= _last_v6_timestamp: + timestamp = _last_v6_timestamp + 1 + _last_v6_timestamp = timestamp + if clock_seq is None: + clock_seq = secrets.randbits(14) # instead of stable storage + time_high_and_time_mid = (timestamp >> 12) & 0xFFFFFFFFFFFF + time_low_and_version = timestamp & 0x0FFF + uuid_int = time_high_and_time_mid << 80 + uuid_int |= time_low_and_version << 64 + uuid_int |= (clock_seq & 0x3FFF) << 48 + uuid_int |= secrets.randbits(48) + return PythonUUID(int=uuid_int, version=6) + + +def uuid7() -> PythonUUID: + r"""UUID version 7 features a time-ordered value field derived from the + widely implemented and well known Unix Epoch timestamp source, the + number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap + seconds excluded. As well as improved entropy characteristics over + versions 1 or 6. + + Implementations SHOULD utilize UUID version 7 over UUID version 1 and + 6 if possible.""" + + global _last_v7_timestamp + + nanoseconds = time.time_ns() + timestamp_ms, _ = divmod(nanoseconds, 10**6) + if _last_v7_timestamp is not None and timestamp_ms <= _last_v7_timestamp: + timestamp_ms = _last_v7_timestamp + 1 + _last_v7_timestamp = timestamp_ms + uuid_int = (timestamp_ms & 0xFFFFFFFFFFFF) << 80 + uuid_int |= secrets.randbits(76) + return PythonUUID(int=uuid_int, version=7) + + +def uuid8() -> PythonUUID: + r"""UUID version 8 features a time-ordered value field derived from the + widely implemented and well known Unix Epoch timestamp source, the + number of nanoseconds seconds since midnight 1 Jan 1970 UTC, leap + seconds excluded.""" + + global _last_v8_timestamp + + nanoseconds = time.time_ns() + if _last_v8_timestamp is not None and nanoseconds <= _last_v8_timestamp: + nanoseconds = _last_v8_timestamp + 1 + _last_v8_timestamp = nanoseconds + timestamp_ms, timestamp_ns = divmod(nanoseconds, 10**6) + subsec = _subsec_encode(timestamp_ns) + subsec_a = subsec >> 8 + subsec_b = subsec & 0xFF + uuid_int = (timestamp_ms & 0xFFFFFFFFFFFF) << 80 + uuid_int |= subsec_a << 64 + uuid_int |= subsec_b << 54 + uuid_int |= secrets.randbits(54) + return PythonUUID(int=uuid_int, version=8) diff --git a/uprotocol/uuid/factory/uuidfactory.py b/uprotocol/uuid/factory/uuidfactory.py new file mode 100644 index 0000000..2b62b97 --- /dev/null +++ b/uprotocol/uuid/factory/uuidfactory.py @@ -0,0 +1,77 @@ +# ------------------------------------------------------------------------- +import hashlib +import random +import struct +from datetime import datetime +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from enum import Enum + +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.uuid.factory import * +from uprotocol.uuid.factory.uuidutils import UUIDUtils + + +class UUIDFactory: + def create(self, instant=None): + if instant is None: + instant = datetime.now() + return self._create(instant) + + def _create(self, instant): + pass + + +class UUIDv6Factory(UUIDFactory): + + def _create(self, instant) -> UUID: + + python_uuid = uuid6() + msb, lsb = UUIDUtils.get_msb_lsb(python_uuid) + return UUID(msb=msb, lsb=lsb) + + +class UUIDv8Factory(UUIDFactory): + MAX_COUNT = 0xfff + _lsb = (random.getrandbits(63) & 0x3fffffffffffffff) | 0x8000000000000000 + UUIDV8_VERSION = 8 + _msb = UUIDV8_VERSION << 12 + def _create(self, instant) -> UUID: + time = int(instant.timestamp() * 1000) if instant else int(datetime.now().timestamp() * 1000) + + if time == (self._msb >> 16): + if (self._msb & 0xFFF) < self.MAX_COUNT: + self._msb += 1 + else: + self._msb = (time << 16) | (8 << 12) + + return UUID(msb=self._msb, lsb=self._lsb) + # return UUID(msb=msb, lsb=lsb) + + +class Factories(): + UUIDV6 = UUIDv6Factory() + UPROTOCOL = UUIDv8Factory() diff --git a/uprotocol/uuid/factory/uuidutils.py b/uprotocol/uuid/factory/uuidutils.py new file mode 100644 index 0000000..8ef0c0e --- /dev/null +++ b/uprotocol/uuid/factory/uuidutils.py @@ -0,0 +1,166 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import uuid +from datetime import datetime +from enum import Enum +from typing import Optional + + +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.uuid.factory import PythonUUID + + +class Version(Enum): + """ + UUID Version + """ + VERSION_UNKNOWN = 0 # An unknown version. + VERSION_RANDOM_BASED = 4 # The randomly or pseudo-randomly generated version specified in RFC-4122. + VERSION_TIME_ORDERED = 6 # The time-ordered version with gregorian epoch proposed by Peabody and Davis. + VERSION_UPROTOCOL = 8 # The custom or free-form version proposed by Peabody and Davis. + + @staticmethod + def getVersion(value: int): + """ + Get the Version from the passed integer representation of the version.

+ @param value:The integer representation of the version. + @return:The Version object or Optional.empty() if the value is not a valid version. + """ + for version in Version: + if version.value == value: + return version + return None + + +class UUIDUtils: + """ + UUID Utils class that provides utility methods for uProtocol IDs + """ + + @staticmethod + def getVersion(uuid_obj: UUID) -> Optional[Version]: + """ + Fetch the UUID version.

+ @param uuid_obj:The UUID to fetch the version from. + @return: Return the UUID version from the UUID object or Optional.empty() if the uuid is null. + """ + if uuid_obj is None: + return None + + return Version.getVersion((uuid_obj.msb >> 12) & 0x0f) + + + @staticmethod + def getVariant(uuid_obj: UUID) -> Optional[str]: + """ + Fetch the Variant from the passed UUID.

+ @param uuid_obj:The UUID to fetch the variant from. + @return:UUID variant or Empty if uuid is null. + """ + if uuid_obj is None: + return None + python_uuid = UUIDUtils.create_pythonuuid_from_eclipseuuid(uuid_obj) + + return python_uuid.variant + + @staticmethod + def isUProtocol(uuid_obj: UUID) -> bool: + """ + Verify if version is a formal UUIDv8 uProtocol ID.

+ @param uuid_obj:UUID object + @return:true if is a uProtocol UUID or false if uuid passed is null or the UUID is not uProtocol format. + """ + + return UUIDUtils.getVersion(uuid_obj) == Version.VERSION_UPROTOCOL if uuid_obj is not None else False + + @staticmethod + def isUuidv6(uuid_obj: UUID) -> bool: + """ + Verify if version is UUIDv6

+ @param uuid_obj:UUID object + @return:true if is UUID version 6 or false if uuid is null or not version 6 + """ + if uuid_obj is None: + return False + + return UUIDUtils.getVersion(uuid_obj) == Version.VERSION_TIME_ORDERED and UUIDUtils.getVariant( + uuid_obj) == uuid.RFC_4122 if uuid_obj is not None else False + + @staticmethod + def isuuid(uuid_obj: UUID) -> bool: + """ + Verify uuid is either v6 or v8

+ @param uuid_obj: UUID object + @return:true if is UUID version 6 or 8 + """ + + return UUIDUtils.isUProtocol(uuid_obj) or UUIDUtils.isUuidv6(uuid_obj) if uuid_obj is not None else False + + @staticmethod + def getTime(uuid: UUID): + """ + Return the number of milliseconds since unix epoch from a passed UUID.

+ @param uuid:passed uuid to fetch the time. + @return:number of milliseconds since unix epoch or empty if uuid is null. + """ + time = None + version = UUIDUtils.getVersion(uuid) + if uuid is None or version is None: + return None + + if version == Version.VERSION_UPROTOCOL: + time = uuid.msb >> 16 + elif version == Version.VERSION_TIME_ORDERED: + try: + python_uuid = UUIDUtils.create_pythonuuid_from_eclipseuuid(uuid) + # Convert 100-nanoseconds ticks to milliseconds + time = python_uuid.time // 10000 + except ValueError: + return None + + return time + + @staticmethod + def get_msb_lsb(uuid: PythonUUID): + # Convert UUID to a 128-bit integer + uuid_int = int(uuid) + + # Extract most significant bits (first 64 bits) + msb = uuid_int >> 64 + + # Extract least significant bits (last 64 bits) + lsb = uuid_int & ((1 << 64) - 1) + + return msb, lsb + + @staticmethod + def create_pythonuuid_from_eclipseuuid(uuid:UUID) -> PythonUUID: + combined_int = (uuid.msb << 64) + uuid.lsb + return PythonUUID(int=combined_int) + # from uprotocol.uuid.serializer.longuuidserializer import LongUuidSerializer + # return PythonUUID(hex=LongUuidSerializer.instance().serialize(uuid)) diff --git a/uprotocol/uuid/serializer/__init__.py b/uprotocol/uuid/serializer/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/uuid/serializer/longuuidserializer.py b/uprotocol/uuid/serializer/longuuidserializer.py new file mode 100644 index 0000000..239bb65 --- /dev/null +++ b/uprotocol/uuid/serializer/longuuidserializer.py @@ -0,0 +1,67 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.uuid.factory import PythonUUID +from uprotocol.uuid.factory.uuidutils import UUIDUtils +from uprotocol.uuid.serializer.uuidserializer import UuidSerializer + + +class LongUuidSerializer(UuidSerializer): + """ + UUID Serializer implementation used to serialize/deserialize UUIDs to/from a string + """ + + @staticmethod + def instance(): + return LongUuidSerializer() + + def deserialize(self, string_uuid: str) -> UUID: + """ + Deserialize from the string format to a UUID. + :param string_uuid: Serialized UUID in string format. + :return: Returns a UUID object from the serialized format from the wire. + """ + if not string_uuid or string_uuid.isspace(): + return UUID() # Return default UUID if string is empty or whitespace + try: + msb, lsb = UUIDUtils.get_msb_lsb(PythonUUID(string_uuid)) + return UUID(msb=msb, lsb=lsb) + except ValueError: + return UUID() # Return default UUID in case of parsing failure + + def serialize(self, uuid: UUID) -> str: + """ + Serialize from a UUID to a string format. + :param uuid: UUID object to be serialized to a string. + :return: Returns the UUID in the string serialized format. + """ + if uuid is None: + return '' + + pythonuuid = UUIDUtils.create_pythonuuid_from_eclipseuuid(uuid) + return str(pythonuuid) if uuid else '' diff --git a/uprotocol/uuid/serializer/microuuidserializer.py b/uprotocol/uuid/serializer/microuuidserializer.py new file mode 100644 index 0000000..0b55bfe --- /dev/null +++ b/uprotocol/uuid/serializer/microuuidserializer.py @@ -0,0 +1,66 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +import struct + +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.uuid.factory.uuidutils import UUIDUtils +from uprotocol.uuid.serializer.uuidserializer import UuidSerializer + + +class MicroUuidSerializer(UuidSerializer): + """ + UUID Serializer implementation used to serialize/deserialize UUIDs to/from bytes + """ + + @staticmethod + def instance(): + return MicroUuidSerializer() + + def deserialize(self, uuid_bytes: bytes) -> UUID: + """ + Deserialize from the bytes format to a UUID. + :param uuid_bytes: Serialized UUID in bytes format. + :return: Returns a UUID object from the serialized format from the wire. + """ + if not uuid_bytes or len(uuid_bytes) != 16: + return UUID() # Return default UUID if bytes are empty or not 16 bytes + + msb, lsb = struct.unpack('>QQ', uuid_bytes) + return UUID(msb=msb, lsb=lsb) + + def serialize(self, uuid: UUID) -> bytes: + """ + Serialize from a UUID to bytes format. + :param uuid: UUID object to be serialized to bytes. + :return: Returns the UUID in the bytes serialized format. + """ + if uuid is None: + return bytearray() + pythonuuid = UUIDUtils.create_pythonuuid_from_eclipseuuid(uuid) + msb, lsb = divmod(pythonuuid.int, 2 ** 64) + return struct.pack('>QQ', msb, lsb) diff --git a/uprotocol/uuid/serializer/uuidserializer.py b/uprotocol/uuid/serializer/uuidserializer.py new file mode 100644 index 0000000..44356d4 --- /dev/null +++ b/uprotocol/uuid/serializer/uuidserializer.py @@ -0,0 +1,57 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod +from typing import TypeVar, Generic + +from uprotocol.proto.uuid_pb2 import UUID + +T = TypeVar('T') + + +class UuidSerializer(ABC, Generic[T]): + """ + UUID Serializer interface used to serialize/deserialize UUIDs to/from either Long (string) or micro (bytes) form + """ + + @abstractmethod + def deserialize(self, uuid: T) -> UUID: + """ + Deserialize from the format to a UUID. + :param uuid: Serialized UUID. + :return: Returns a UUID object from the serialized format from the wire. + """ + pass # Implement your deserialization logic here + + @abstractmethod + def serialize(self, uuid: UUID) -> T: + """ + Serialize from a UUID to a specific serialization format. + :param uuid: UUID object to be serialized to the format T. + :return: Returns the UUID in the transport serialized format. + """ + pass # Implement your serialization logic here diff --git a/uprotocol/uuid/validate/__init__.py b/uprotocol/uuid/validate/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/uuid/validate/uuidvalidator.py b/uprotocol/uuid/validate/uuidvalidator.py new file mode 100644 index 0000000..dce36cd --- /dev/null +++ b/uprotocol/uuid/validate/uuidvalidator.py @@ -0,0 +1,119 @@ +# ------------------------------------------------------------------------- +from abc import ABC, abstractmethod +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from collections import namedtuple +from enum import Enum + +from uprotocol.proto.uuid_pb2 import UUID +from uprotocol.uuid.factory.uuidutils import UUIDUtils, Version +from uprotocol.validation.validationresult import ValidationResult +from uprotocol.proto.ustatus_pb2 import UStatus, UCode + + +class UuidVariant(Enum): + VARIANT_RFC_4122 = "RFC 4122" + + +class UuidValidator(ABC): + """ + UUID Validator class that validates UUIDs + """ + + @staticmethod + def get_validator(uuid: UUID): + if UUIDUtils.isUuidv6(uuid): + return Validators.UUIDV6.validator() + elif UUIDUtils.isUProtocol(uuid): + return Validators.UPROTOCOL.validator() + else: + return Validators.UNKNOWN.validator() + + def validate(self, uuid: UUID) -> UStatus: + error_messages = [self.validate_version(uuid), self.validate_variant(uuid), self.validate_time(uuid)] + error_messages = [result.get_message() for result in error_messages if result.is_failure()] + error_message = ",".join(error_messages) + if not error_message: + return ValidationResult.success().to_status() + return UStatus(code=UCode.INVALID_ARGUMENT, message=error_message) + + @abstractmethod + def validate_version(self, uuid: UUID) -> ValidationResult: + raise NotImplementedError + + def validate_time(self, uuid: UUID) -> ValidationResult: + time = UUIDUtils.getTime(uuid) + return ValidationResult.success() if (time is not None and time > 0 )else ValidationResult.failure("Invalid UUID Time") + + @abstractmethod + def validate_variant(self, uuid: UUID) -> ValidationResult: + raise NotImplementedError + + +class InvalidValidator(UuidValidator): + def validate_version(self, uuid: UUID) -> ValidationResult: + return ValidationResult.failure("Invalid UUID Version") + + def validate_variant(self, uuid: UUID) -> ValidationResult: + return ValidationResult.failure("Invalid UUID Variant") + + +class UUIDv6Validator(UuidValidator): + def validate_version(self, uuid: UUID) -> ValidationResult: + version = UUIDUtils.getVersion(uuid) + return ValidationResult.success() if version and version == Version.VERSION_TIME_ORDERED else ( + ValidationResult.failure( + "Not a UUIDv6 Version")) + + def validate_variant(self, uuid: UUID) -> ValidationResult: + variant = UUIDUtils.getVariant(uuid) + return ValidationResult.success() if variant and "RFC 4122" in variant else ValidationResult.failure( + "Invalid UUIDv6 variant") + + +class UUIDv8Validator(UuidValidator): + def validate_version(self, uuid: UUID) -> ValidationResult: + version = UUIDUtils.getVersion(uuid) + return ValidationResult.success() if version and version == Version.VERSION_UPROTOCOL else ( + ValidationResult.failure( + "Invalid UUIDv8 Version")) + + def validate_variant(self, uuid: UUID) -> ValidationResult: + return ValidationResult.success() + + +class Validators(Enum): + UNKNOWN = InvalidValidator() # Use a default validator instance + UUIDV6 = UUIDv6Validator() # Use a default validator instance + UPROTOCOL = UUIDv8Validator() # Use a default validator instance + + def validator(self): + return self.value + + def __new__(cls, value): + obj = object.__new__(cls) + obj._value_ = value + return obj diff --git a/uprotocol/validation/__init__.py b/uprotocol/validation/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/uprotocol/validation/validationresult.py b/uprotocol/validation/validationresult.py new file mode 100644 index 0000000..0d8e37c --- /dev/null +++ b/uprotocol/validation/validationresult.py @@ -0,0 +1,109 @@ +# ------------------------------------------------------------------------- + +# Copyright (c) 2023 General Motors GTO LLC +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# SPDX-FileType: SOURCE +# SPDX-FileCopyrightText: 2023 General Motors GTO LLC +# SPDX-License-Identifier: Apache-2.0 + +# ------------------------------------------------------------------------- + + +from abc import ABC, abstractmethod + +from uprotocol.proto.ustatus_pb2 import UCode, UStatus + + + +class ValidationResult(ABC): + """ + Class wrapping a ValidationResult of success or failure wrapping the value of a UStatus. + """ + STATUS_SUCCESS = UStatus(code=UCode.OK, message="OK") + + def __init__(self): + pass + + @abstractmethod + def to_status(self) -> UStatus: + pass + + @abstractmethod + def is_success(self) -> bool: + pass + + def is_failure(self) -> bool: + return not self.is_success() + + @abstractmethod + def get_message(self) -> str: + pass + + @staticmethod + def success(): + return Success() + + @staticmethod + def failure(message): + return Failure(message) + + +class Failure(ValidationResult): + """ + Implementation for failure, wrapping the message. + """ + + def __init__(self, message): + super().__init__() + self.message = message if message else "Validation Failed." + + def to_status(self) -> UStatus: + return UStatus(code=3, message=self.message) + + def is_success(self) -> bool: + return False + + def get_message(self) -> str: + return self.message + + def __str__(self): + return f"ValidationResult.Failure(message='{self.message}')" + + +class Success(ValidationResult): + """ + Implementation for success, wrapping a UStatus with UCode 0 for success. + """ + + def to_status(self) -> UStatus: + return ValidationResult.STATUS_SUCCESS + + def is_success(self) -> bool: + return True + + def get_message(self) -> str: + return "" + + def __str__(self): + return "ValidationResult.Success()" + + def __eq__(self, other): + if isinstance(other, Success): + return self.to_status() == other.to_status() + return False