diff --git a/MANIFEST.in b/MANIFEST.in index 1aba38f..9ee4b5f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include LICENSE +recursive-include dxlbootstrap/generate/templates/app/static/schema * \ No newline at end of file diff --git a/dist.py b/dist.py index 0624c85..b290571 100644 --- a/dist.py +++ b/dist.py @@ -110,7 +110,7 @@ def replace(file_path, pattern, subst): "--universal"]) # cp -rf config dist -print("\nCopying config in to dist directory\n") +print("\nCopying config into dist directory\n") copy_tree(os.path.join(DIST_PY_FILE_LOCATION, "config"), os.path.join(DIST_DIRECTORY, "config")) # Copy everything in to release dir diff --git a/dxlbootstrap/generate/templates/app/static/schema/v0.1/schema.tmpl b/dxlbootstrap/generate/templates/app/static/schema/v0.1/schema.tmpl new file mode 100644 index 0000000..8f1ca2d --- /dev/null +++ b/dxlbootstrap/generate/templates/app/static/schema/v0.1/schema.tmpl @@ -0,0 +1 @@ +${schemaYaml} \ No newline at end of file diff --git a/dxlbootstrap/generate/templates/app/template.py b/dxlbootstrap/generate/templates/app/template.py index 8708306..5e5ffbe 100644 --- a/dxlbootstrap/generate/templates/app/template.py +++ b/dxlbootstrap/generate/templates/app/template.py @@ -4,7 +4,8 @@ import Template, TemplateConfig, TemplateConfigSection, PythonPackageConfigSection from dxlbootstrap.generate.core.component \ import DirTemplateComponent, FileTemplateComponent, CodeTemplateComponent - +from dxlbootstrap.generate.util.schema_utils \ + import DxlSchemaWriter class AppTemplateConfig(TemplateConfig): """ @@ -353,6 +354,83 @@ def _build_sample_directory(context, components_dict): sample_basic_dir.add_child(basic_sample_comp) components_dict["basic_sample_comp"] = basic_sample_comp + @staticmethod + def _copy_schema_files(context, components_dict, dir_comp): + """ + Copies the application schema file to the specified directory + + :param context: The template context + :param components_dict: Dictionary containing components by name (and other info) + :param dir_comp: The directory component to copy the files to + """ + + del components_dict + + config = context.template.template_config + app_name = str(config.application_section.name) + service_names = config.application_section.services + + yaml_writer = DxlSchemaWriter(app_name) + + if service_names: + for service_name in service_names: + service = config.get_service_section(service_name) + service_type = str(service.service_type) + + yaml_writer.add_service_def_to_schema(service_type) + yaml_writer.add_service_ref_to_solution(service_type) + + request_handlers = service.request_handlers + if request_handlers: + for request_handler in request_handlers: + req_handler_def = config.get_request_handler_section(request_handler) + topic = str(req_handler_def.topic) + + yaml_writer.add_request_def_to_schema(topic) + yaml_writer.add_request_ref_to_service(service_type, topic) + + event_handlers = config.application_section.event_handlers + if event_handlers: + for event_handler in event_handlers: + event_handler_def = config.get_event_handler_section(event_handler) + topic = str(event_handler_def.topic) + + yaml_writer.add_event_def_to_schema(topic) + yaml_writer.add_event_ref_to_solution(topic) + + schema_dict = yaml_writer.schema_dict_yaml + + file_comp = FileTemplateComponent( + app_name + ".yaml", + "schema/v0.1/schema.tmpl", + { + "schemaContent": schema_dict + } + ) + dir_comp.add_child(file_comp) + + @staticmethod + def _build_schema_directory(context, components_dict): + """ + Builds the "schema" directory components of the output + + :param context: The template context + :param components_dict: Dictionary containing components by name (and other info) + :return: The "schema" directory components of the output + """ + + root = components_dict["root"] + + schema_dir = DirTemplateComponent("schema") + root.add_child(schema_dir) + + schema_version_dir = DirTemplateComponent("v0.1") + schema_dir.add_child(schema_version_dir) + + components_dict["schema_comp"] = schema_dir + + AppTemplate._copy_schema_files(context, components_dict, schema_version_dir) + def _build_docs_directory(self, context, components_dict): """ Builds the "docs" directory components of the output @@ -621,6 +699,7 @@ def _get_root_component(self, context): self._build_root_directory(context, components_dict) self._build_config_directory(context, components_dict) self._build_sample_directory(context, components_dict) + self._build_schema_directory(context, components_dict) self._build_docs_directory(context, components_dict) self._build_application_directory(context, components_dict) self._build_event_handlers(context, components_dict) diff --git a/dxlbootstrap/generate/util/__init__.py b/dxlbootstrap/generate/util/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/dxlbootstrap/generate/util/schema_utils.py b/dxlbootstrap/generate/util/schema_utils.py new file mode 100644 index 0000000..1d1093d --- /dev/null +++ b/dxlbootstrap/generate/util/schema_utils.py @@ -0,0 +1,177 @@ +# -*- coding: utf-8 -*- +################################################################################ +# Copyright (c) 2019 McAfee LLC - All Rights Reserved. +################################################################################ + +import copy +import yaml + +# The OpenDXL API Specification version +SCHEMA_VERSION = "0.1" + +# Keys to be used for schema objects/definitions +SOLUTION_KEY = "solutions" +SERVICE_KEY = "services" +EVENT_KEY = "events" +REQUEST_KEY = "requests" + +# Service definition +SERVICE_DICT_TMPL = { + "info": { + #"title" + "version": "", + "description": "This is a description of this service." + }, + "externalDocs": { + "description": "", + "url": "http://opendxl.com" + }, + REQUEST_KEY: [] +} + +# Base message definition +MESSAGE_PROPS_DICT_TMPL = { + "description": "This is a description of this message.", + "payload": { + "properties": { + "property1": { + "description": "This is a description of this property.", + "type": "" + }, + "property2": { + "description": "This is a description of this property.", + "type": "" + } + }, + "example": { + "property1": "", + "property2": "" + } + } +} + +# Request definition +REQUEST_DICT_TMPL = copy.deepcopy(MESSAGE_PROPS_DICT_TMPL) +REQUEST_DICT_TMPL["response"] = copy.deepcopy(MESSAGE_PROPS_DICT_TMPL) + +# Event definition +EVENT_DICT_TMPL = copy.deepcopy(MESSAGE_PROPS_DICT_TMPL) +EVENT_DICT_TMPL["isIncoming"] = True + + +def topic_ref_transform(ref_string, topic): + """ + Converts a topic string with forward-slashes into a JSON reference-friendly format + ("/" becomes "~1"), then places the new string into the provided base reference path. + + :param ref_string: The base reference path + :param topic: The DXL topic string to convert + """ + return str(ref_string.format(topic.replace("/", "~1"))) + + +class DxlSchemaWriter(object): + + def __init__(self, app_name): + """ + Constructs the schema writer. + + :param app_name: The application name + """ + + # Define the app name and the general template for the schema + self.app_name = app_name + self.schema_dict_tmpl = { + "openDxlApi": SCHEMA_VERSION, + "info": { + "title": app_name, + "version": "0.1", + "description": "This is a general description of this API specification.", + "contact": { + "url": "www.company-website-url.com" + } + }, + SOLUTION_KEY: { + app_name: { + "info": { + "title": app_name, + "version": "", + "description": "This is a description of this solution." + }, + "externalDocs": { + "description": "This is a link to further documentation for this solution.", + "url": "http://opendxl.com" + }, + SERVICE_KEY: [], + EVENT_KEY: [] + } + }, + SERVICE_KEY: {}, + EVENT_KEY: {}, + REQUEST_KEY: {} + } + + @property + def schema_dict_yaml(self): + """ + Returns the current constructed version of the schema. + """ + return yaml.dump(self.schema_dict_tmpl, default_flow_style=False) + + def add_service_ref_to_solution(self, service_type): + """ + Adds a service reference to a solution's "services" array. + + :param service_type: The service type + """ + self.schema_dict_tmpl[SOLUTION_KEY][self.app_name][SERVICE_KEY]\ + .append({"$ref": topic_ref_transform("#/services/{0}", service_type)}) + + def add_event_ref_to_solution(self, event_topic): + """ + Adds an event message reference to a solution's "events" array. + + :param event_topic: The DXL topic on which the event is sent + """ + self.schema_dict_tmpl[SOLUTION_KEY][self.app_name][EVENT_KEY]\ + .append({"$ref": topic_ref_transform("#/events/{0}", event_topic)}) + + def add_request_ref_to_service(self, service_type, request_topic): + """ + Adds a request message reference to a service's "requests" array. + + :param service_type: The DXL topic on which the event is sent + :param request_topic: The DXL topic on which the request is sent + """ + self.schema_dict_tmpl[SERVICE_KEY][service_type][REQUEST_KEY]\ + .append({"$ref": topic_ref_transform("#/requests/{0}", request_topic)}) + + def add_service_def_to_schema(self, service_type): + """ + Adds a service definition to the schema. Uses the service type as a + key for the service definition object. + + :param service_type: The service type + """ + self.schema_dict_tmpl[SERVICE_KEY][service_type] = \ + copy.deepcopy(SERVICE_DICT_TMPL) + + def add_request_def_to_schema(self, request_topic): + """ + Adds a request message definition to the schema. Uses the request + topic as a key for the request definition object. + + :param request_topic: The DXL topic on which the request is sent + """ + self.schema_dict_tmpl[REQUEST_KEY][request_topic] = \ + copy.deepcopy(REQUEST_DICT_TMPL) + + def add_event_def_to_schema(self, event_topic): + """ + Adds an event message definition to the schema. Uses the event + topic as a key for the event definition object. + + :param event_topic: The DXL topic on which the event is sent + """ + self.schema_dict_tmpl[EVENT_KEY][event_topic] = \ + copy.deepcopy(EVENT_DICT_TMPL) diff --git a/setup.py b/setup.py index f925397..e49c44c 100644 --- a/setup.py +++ b/setup.py @@ -62,7 +62,8 @@ def run(self): # Requirements install_requires=[ - "dxlclient>=4.1.0.184" + "dxlclient>=4.1.0.184", + "pyyaml>=5.1" ], tests_require=TEST_REQUIREMENTS, @@ -106,11 +107,14 @@ def run(self): "dxlbootstrap.generate.templates.client.static.sample.basic", "dxlbootstrap.generate.templates.client.static.sample.basic.code", "dxlbootstrap.generate.templates.client.static.doc", - "dxlbootstrap.generate.templates.client.static.doc.sdk" + "dxlbootstrap.generate.templates.client.static.doc.sdk", + "dxlbootstrap.generate.util" ], package_data={'': ['*.tmpl']}, + include_package_data=True, + # Details url="http://www.mcafee.com/", diff --git a/tests/test_dxlbootstrap.py b/tests/test_dxlbootstrap.py index e1bc612..35fce56 100644 --- a/tests/test_dxlbootstrap.py +++ b/tests/test_dxlbootstrap.py @@ -72,6 +72,8 @@ def test_generate_application_command(self): mock_print.assert_called_with("Generation succeeded.") self.assertTrue(os.path.exists( os.path.join(app_dir, "geolocationservice", "app.py"))) + self.assertTrue(os.path.exists( + os.path.join(app_dir, "schema/v0.1", "geolocationservice.yaml"))) def test_generate_client_command(self): with _TempDir("genclient") as temp_dir, \