diff --git a/README.md b/README.md index 94d3ac8..1e8ab5d 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,9 @@ Options: contains more than one ApiGateway resource) -f, --format [yaml|json] output format [default: yaml] -i, --indent INTEGER if output format is json, specify indentation + -d, --debug if enabled, show debug logs + --no-ignore if set, deforest will export paths marked as + ignored --version Show the version and exit. --help Show this message and exit. ``` @@ -33,6 +36,97 @@ Options: Version 0.1.1 and later supports CloudFormation templates as input. If more than one API Gateway is part of the template, the `--outfile` flag will be ignored. +## Hide paths + +Version 0.2.0 introduced support for deforest to ignore certain paths. If you specify `x-deforest-ignore: true` anywhere in your specification, deforest will not extract its _parent_ node to the end results. Example: + +```yaml +paths: + "/validation": + post: + responses: + "200": + schema: + type: array + items: + "$ref": "#/definitions/Error" + headers: + test-method-response-header: + type: string + get: + x-deforest-ignore: true + parameters: + - name: q1 + in: query + required: true + responses: + "200": + schema: + type: array + items: + "$ref": "#/definitions/Error" + headers: + test-method-response-header: + type: string +``` + +will result in + +```yaml +paths: + /validation: + post: + responses: + "200": + headers: + test-method-response-header: + type: string + schema: + items: + $ref: "#/definitions/Error" + type: array +``` + +If we'd written this: + +```yaml +paths: + "/validation": + x-deforest-ignore: true + post: + responses: + "200": + schema: + type: array + items: + "$ref": "#/definitions/Error" + headers: + test-method-response-header: + type: string + get: + parameters: + - name: q1 + in: query + required: true + responses: + "200": + schema: + type: array + items: + "$ref": "#/definitions/Error" + headers: + test-method-response-header: + type: string +``` + +we'd get an empty result since the _parent_ node is removed: + +```yaml +paths: {} +``` + +If `x-deforest-ignore: false`, or missing, the node will be extracted as usual. If the runtime flag `--no-ignore` is set, the nodes will be extracted as usual as well. + # Limitations The output file looses its order of the keys in the file, which shouldn't affect you if you're using a converter to create a graphical documentation/specification - but can be confusing if you have a specific internal order you wish to keep. diff --git a/deforest/__init__.py b/deforest/__init__.py index e69de29..57c2dd0 100644 --- a/deforest/__init__.py +++ b/deforest/__init__.py @@ -0,0 +1,2 @@ +VERSION = "0.1.1" +LOGGER = "deforest" diff --git a/deforest/cfcleaner.py b/deforest/cfcleaner.py deleted file mode 100644 index f40d0b2..0000000 --- a/deforest/cfcleaner.py +++ /dev/null @@ -1,97 +0,0 @@ -from tags import GetAttTag, SubTag, RefTag -import yaml -import re - -class CFCleaner(): - keys = ["x-amazon"] - filedata = None - raw = {} - processed = None - KEY_TYPE = "Type" - KEY_RESOURCES = "Resources" - KEY_PROPERTIES = "Properties" - KEY_BODY = "Body" - TYPE_APIGW = "AWS::ApiGateway::RestApi" - - multi_raw = [] - multi_processed = [] - - def __init__(self,data): - self.filedata = data - - def convert(self): - self._enable_custom_tags() - self._load() - self._clean_all_keys() - self._dump_all() - return self.multi_processed - - def number_results(self): - return len(self.multi_processed) - - def _clean_all_keys(self): - self._identify_apigw(self.raw) - - def _identify_apigw(self,v): - resources = v[CFCleaner.KEY_RESOURCES] - for k in resources: - if isinstance(resources[k], dict): - if self._is_apigw(resources[k]) and self._has_properties_and_body(resources[k]): - self.multi_raw.append(resources[k][CFCleaner.KEY_PROPERTIES][CFCleaner.KEY_BODY]) - - for raw in self.multi_raw: - self._cleanup_keys(raw) - - def _cleanup_keys(self,v): - for k in v.keys(): - if any(m in k for m in self.keys): - del v[k] - else: - if isinstance(v[k], dict): - self._cleanup_keys(v[k]) - - def _is_apigw(self,node): - if CFCleaner.KEY_TYPE in node: - if node[CFCleaner.KEY_TYPE] == CFCleaner.TYPE_APIGW: - return True - return False - - def _has_properties_and_body(self,node): - return CFCleaner.KEY_PROPERTIES in node and CFCleaner.KEY_BODY in node[CFCleaner.KEY_PROPERTIES] - - def _namify(self, title, version): - s = "{}-{}".format(title.lower(),version.lower()) - s = re.sub(r"\s+", '-', s) - return s - - def get_title_and_version_all(self,index): - title = self.multi_raw[index]["info"]["title"] or "no-title" - version = self.multi_raw[index]["info"]["version"] or "no-version" - return self._namify(title,version) - - def get_title_and_version(self): - return self.get_title_and_version(0) - - def _enable_custom_tags(self): - yaml.SafeLoader.add_constructor('!GetAtt', GetAttTag.from_yaml) - yaml.SafeLoader.add_constructor('!Sub', SubTag.from_yaml) - yaml.SafeLoader.add_constructor('!Ref', RefTag.from_yaml) - yaml.SafeDumper.add_multi_representer(GetAttTag, GetAttTag.to_yaml) - yaml.SafeDumper.add_multi_representer(SubTag, SubTag.to_yaml) - yaml.SafeDumper.add_multi_representer(RefTag, RefTag.to_yaml) - - def _load(self): - self.raw = yaml.safe_load(self.filedata) - - def _dump(self): - self.processed = yaml.safe_dump(self.multi_raw[0]) - - def _dump_all(self): - for r in self.multi_raw: - self.multi_processed.append(yaml.safe_dump(r)) - - def get_raw(self,index): - return self.multi_raw[0] - - def get_raw_all(self,index): - return self.multi_raw[index] \ No newline at end of file diff --git a/deforest/cleaner.py b/deforest/cleaner.py deleted file mode 100644 index 92bb743..0000000 --- a/deforest/cleaner.py +++ /dev/null @@ -1,66 +0,0 @@ -from tags import GetAttTag, SubTag, RefTag -import yaml -import re - -class DeforestCleaner(): - keys = ["x-amazon"] - filedata = None - raw = {} - processed = None - - def __init__(self, data): - self.filedata = data - - def _namify(self, title, version): - s = "{}-{}".format(title.lower(),version.lower()) - s = re.sub(r"\s+", '-', s) - return s - - def get_title_and_version(self): - title = self.raw["info"]["title"] or "no-title" - version = self.raw["info"]["version"] or "no-version" - return self._namify(title,version) - - def get_title_and_version_all(self,index): - return self.get_title_and_version() - - def get_raw(self): - return self.raw - - def get_raw_all(self,index): - return self.raw - - def convert(self): - self._enable_custom_tags() - self._load() - self._clean_all_keys() - self._dump() - return [self.processed] - - def _clean_all_keys(self): - self._cleanup_keys(self.raw) - - def _cleanup_keys(self, v): - for k in v.keys(): - if any(m in k for m in self.keys): - del v[k] - else: - if isinstance(v[k], dict): - self._cleanup_keys(v[k]) - - def _enable_custom_tags(self): - yaml.SafeLoader.add_constructor('!GetAtt', GetAttTag.from_yaml) - yaml.SafeLoader.add_constructor('!Sub', SubTag.from_yaml) - yaml.SafeLoader.add_constructor('!Ref', RefTag.from_yaml) - yaml.SafeDumper.add_multi_representer(GetAttTag, GetAttTag.to_yaml) - yaml.SafeDumper.add_multi_representer(SubTag, SubTag.to_yaml) - yaml.SafeDumper.add_multi_representer(RefTag, RefTag.to_yaml) - - def _load(self): - self.raw = yaml.safe_load(self.filedata) - - def _dump(self): - self.processed = yaml.safe_dump(self.raw) - - def number_results(self): - return 1 \ No newline at end of file diff --git a/deforest/cleaners.py b/deforest/cleaners.py new file mode 100644 index 0000000..2ad618c --- /dev/null +++ b/deforest/cleaners.py @@ -0,0 +1,90 @@ +import logging + + +class CloudFormationCleaner: + KEY_CF_FIELD = "AWSTemplateFormatVersion" + KEY_CF_RESOURCES = "Resources" + KEY_CF_BODY = "Body" + KEY_CF_TYPE = "Type" + KEY_CF_PROPERTIES = "Properties" + CF_APIGW_TYPE = "AWS::ApiGateway::RestApi" + + def __init__(self, caller): + logging.debug("created {}".format(self)) + self.caller = caller + + def clean(self): + logging.debug("{} clean".format(self)) + logging.debug("checking {}".format(self.caller.result)) + if not CloudFormationCleaner.KEY_CF_FIELD in self.caller.result: + self.caller.result = [self.caller.result] + return + rval = [] + copy = self.caller.result + res = copy[CloudFormationCleaner.KEY_CF_RESOURCES] + for k in res: + if isinstance(res[k], dict): + if self._is_apigw(res[k]) and self._has_body(res[k]): + logging.info( + "identified resource '{}' as an API Gateway".format(k)) + rval.append( + res[k][CloudFormationCleaner.KEY_CF_PROPERTIES][CloudFormationCleaner.KEY_CF_BODY]) + self.caller.result = rval + + def _is_apigw(self, node): + if CloudFormationCleaner.KEY_CF_TYPE in node: + return node[CloudFormationCleaner.KEY_CF_TYPE] == CloudFormationCleaner.CF_APIGW_TYPE + return False + + def _has_body(self, node): + return CloudFormationCleaner.KEY_CF_PROPERTIES in node and CloudFormationCleaner.KEY_CF_BODY in node[CloudFormationCleaner.KEY_CF_PROPERTIES] + + +class DefaultCleaner: + keys = ["x-amazon"] + copy = None + + def __init__(self, caller): + logging.debug("created {}".format(self)) + self.caller = caller + + def clean(self): + logging.debug("{} clean".format(self)) + logging.debug("checking {}".format(self.caller.result)) + self.copy = self.caller.result + for i in self.copy: + self._clean(i) + self.caller.result = self.copy + + def _clean(self, v): + for k in v.keys(): + if any(m in k for m in self.keys): + del v[k] + else: + if isinstance(v[k], dict): + self._clean(v[k]) + + +class IgnoreCleaner: + DEFOREST_IGNORE_KEY = "x-deforest-ignore" + copy = None + + def __init__(self, caller): + logging.debug("created {}".format(self)) + self.caller = caller + + def clean(self): + self.copy = self.caller.result + for i in self.copy: + self._clean(i) + self.caller.result = self.copy + + def _clean(self, v): + for k in v.keys(): + if isinstance(v[k], dict): + if IgnoreCleaner.DEFOREST_IGNORE_KEY in v[k]: + if v[k][IgnoreCleaner.DEFOREST_IGNORE_KEY] is True: + logging.debug("removing {}".format(v[k])) + del v[k] + else: + self._clean(v[k]) diff --git a/deforest/constant.py b/deforest/constant.py index 57c2dd0..99e4c7b 100644 --- a/deforest/constant.py +++ b/deforest/constant.py @@ -1,2 +1,4 @@ -VERSION = "0.1.1" +VERSION = "0.2.0" LOGGER = "deforest" +EXIT_NOTFOUND = 1 +EXIT_PARSEERR = 2 diff --git a/deforest/deforest.py b/deforest/deforest.py index bdb02fe..c6f9503 100644 --- a/deforest/deforest.py +++ b/deforest/deforest.py @@ -1,36 +1,52 @@ +import coloredlogs +import logging import click -import json +import sys +from constant import VERSION, LOGGER, EXIT_NOTFOUND +from filecleaner import ForestCleaner +from filecreator import FileCreator -from constant import VERSION, LOGGER -from cleaner import DeforestCleaner -from solution import Solution @click.command() @click.argument("infile") -@click.option("--outfile","-o", help="specify output file, default is ./