diff --git a/docs/trestle_author.md b/docs/trestle_author.md index 6c5c9bcb8..db3f94721 100644 --- a/docs/trestle_author.md +++ b/docs/trestle_author.md @@ -409,6 +409,61 @@ Running `trestle author docs validate -tn docs_task -gh="Governed section"` will - If `--template-version 1.0.0` (`-tv`) is passed the header field `x-trestle-template-version` will be ignored and document will be forcefully validated against template of version `1.0.0`. Use this for testing purposes _only_ when you need to validate the document against a specific template. By default the template version will be determined based on `x-trestle-template-version` in the document. +### Validating the documents against different templates + +Validation against multiple templates as stated before can be done, but there is another scenario that you can leverage on trestle to have multiple documents in the task folder corresponding to a single template. + +For that to happen you will need to provide your template with the following parameter at the yaml header level, matching the type of template to be implemented so the validation can occur: + +> x-trestle-template-type: insert_template_type_here + +Please, take into consideration that for the validation to happen you will also need to provide each instance document in the task folder a field called `x-trestle-template-type: insert_template_type_here` in the yaml header matching with the template name. + +```yaml +--- +authors: tmp +owner: tmp +valid: + from: null + to: null +x-trestle-template-type: insert_template_type_here +--- +``` + +With that, you will be able to create more than 1 instance document per template and give the instance the desired name. + +For instance, let´s consider the next folder structure: + +```text +trestle_root +┣ .trestle +┃ ┣ author +┃ ┃ ┣ my_task_2 +┃ ┃ ┃ ┣ 0.0.1 +┃ ┃ ┃ ┃ ┣ a_template.md +┃ ┃ ┃ ┃ ┣ another_template.md +┃ ┃ ┃ ┃ ┗ arhitecture.drawio +┃ ┗ config.ini + +trestle_root + ┣ .trestle + ┣ my_task_2 + ┃ ┣ sample_folder_0 + ┃ ┃ ┣ a_template_1.md + ┃ ┃ ┣ a_template_2.md + ┃ ┃ ┣ arhitecture_1.drawio + ┃ ┃ ┗ another_template_123.md + +``` + +If you noticed, names are no longer needed to match with exact template names, and that´s because validation will run through `x-trestle-template-type` field defined at the instance header, not through the name. + +To validate the documents against their respective templates using `x-trestle-template-type`, run: + +> trestle author folders validate -tn my_task_name -vtt + +Now, `-vtt` stands for validate template type. Validate template type option will provide you the ability to have more than 1 instance per template validated. +
diff --git a/tests/data/author/governed_folders/good_instance_with_template_type/architecture_test_1.md b/tests/data/author/governed_folders/good_instance_with_template_type/architecture_test_1.md new file mode 100644 index 000000000..233a1a40f --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_with_template_type/architecture_test_1.md @@ -0,0 +1,23 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +x-trestle-template-type: architecture +--- + +# System architecture + +Here is some content + +## Overview + +And some more + +## Security model + +And even more diff --git a/tests/data/author/governed_folders/good_instance_with_template_type/architecture_test_2.md b/tests/data/author/governed_folders/good_instance_with_template_type/architecture_test_2.md new file mode 100644 index 000000000..233a1a40f --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_with_template_type/architecture_test_2.md @@ -0,0 +1,23 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +x-trestle-template-type: architecture +--- + +# System architecture + +Here is some content + +## Overview + +And some more + +## Security model + +And even more diff --git a/tests/data/author/governed_folders/good_instance_with_template_type/network_test_1.md b/tests/data/author/governed_folders/good_instance_with_template_type/network_test_1.md new file mode 100644 index 000000000..46b5d2803 --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_with_template_type/network_test_1.md @@ -0,0 +1,27 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +x-trestle-template-type: network +--- + +# Network architecture + +Lots of stuff about the network overall including some diagrams. + +## External interconnections + +Here I put a table which describes the connections beyond my audit boundary with 3rd parties. + +## Corporate interconnections + +Here I describe interconnections into corporate systems. + +## Out of scope interconnections + +Here I describe interconnections that are out of scope because they occur outside of the current audit boundary. diff --git a/tests/data/author/governed_folders/good_instance_with_template_type/network_test_2.md b/tests/data/author/governed_folders/good_instance_with_template_type/network_test_2.md new file mode 100644 index 000000000..46b5d2803 --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_with_template_type/network_test_2.md @@ -0,0 +1,27 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +x-trestle-template-type: network +--- + +# Network architecture + +Lots of stuff about the network overall including some diagrams. + +## External interconnections + +Here I put a table which describes the connections beyond my audit boundary with 3rd parties. + +## Corporate interconnections + +Here I describe interconnections into corporate systems. + +## Out of scope interconnections + +Here I describe interconnections that are out of scope because they occur outside of the current audit boundary. diff --git a/tests/data/author/governed_folders/good_instance_without_template_type/architecture_test_1.md b/tests/data/author/governed_folders/good_instance_without_template_type/architecture_test_1.md new file mode 100644 index 000000000..a683e56bf --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_without_template_type/architecture_test_1.md @@ -0,0 +1,22 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +--- + +# System architecture + +Here is some content + +## Overview + +And some more + +## Security model + +And even more diff --git a/tests/data/author/governed_folders/good_instance_without_template_type/architecture_test_2.md b/tests/data/author/governed_folders/good_instance_without_template_type/architecture_test_2.md new file mode 100644 index 000000000..a683e56bf --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_without_template_type/architecture_test_2.md @@ -0,0 +1,22 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +--- + +# System architecture + +Here is some content + +## Overview + +And some more + +## Security model + +And even more diff --git a/tests/data/author/governed_folders/good_instance_without_template_type/network_test_1.md b/tests/data/author/governed_folders/good_instance_without_template_type/network_test_1.md new file mode 100644 index 000000000..1b1d249f2 --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_without_template_type/network_test_1.md @@ -0,0 +1,26 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +--- + +# Network architecture + +Lots of stuff about the network overall including some diagrams. + +## External interconnections + +Here I put a table which describes the connections beyond my audit boundary with 3rd parties. + +## Corporate interconnections + +Here I describe interconnections into corporate systems. + +## Out of scope interconnections + +Here I describe interconnections that are out of scope because they occur outside of the current audit boundary. diff --git a/tests/data/author/governed_folders/good_instance_without_template_type/network_test_2.md b/tests/data/author/governed_folders/good_instance_without_template_type/network_test_2.md new file mode 100644 index 000000000..1b1d249f2 --- /dev/null +++ b/tests/data/author/governed_folders/good_instance_without_template_type/network_test_2.md @@ -0,0 +1,26 @@ +--- +authors: + - Tim + - Jane + - Sally +owner: Joe +valid: + from: 2020-01-01 + to: 2099-12-31 +--- + +# Network architecture + +Lots of stuff about the network overall including some diagrams. + +## External interconnections + +Here I put a table which describes the connections beyond my audit boundary with 3rd parties. + +## Corporate interconnections + +Here I describe interconnections into corporate systems. + +## Out of scope interconnections + +Here I describe interconnections that are out of scope because they occur outside of the current audit boundary. diff --git a/tests/data/author/governed_folders/template_folder_with_template_type/architecture.md b/tests/data/author/governed_folders/template_folder_with_template_type/architecture.md new file mode 100644 index 000000000..31a41f68a --- /dev/null +++ b/tests/data/author/governed_folders/template_folder_with_template_type/architecture.md @@ -0,0 +1,13 @@ +--- +authors: tmp +owner: tmp +valid: + from: null + to: null +x-trestle-template-type: architecture +--- +# System architecture + +## Overview + +## Security model diff --git a/tests/data/author/governed_folders/template_folder_with_template_type/network.md b/tests/data/author/governed_folders/template_folder_with_template_type/network.md new file mode 100644 index 000000000..cdc431123 --- /dev/null +++ b/tests/data/author/governed_folders/template_folder_with_template_type/network.md @@ -0,0 +1,16 @@ +--- +authors: tmp +owner: tmp +valid: + from: + to: +x-trestle-template-type: network +--- + +# Network architecture + +## External interconnections + +## Corporate interconnections + +## Out of scope interconnections diff --git a/tests/trestle/core/commands/author/folders_test.py b/tests/trestle/core/commands/author/folders_test.py index cd47e9f5e..36f958f43 100644 --- a/tests/trestle/core/commands/author/folders_test.py +++ b/tests/trestle/core/commands/author/folders_test.py @@ -676,6 +676,66 @@ def test_drawio_versioning_validation( assert rc == 0 +def test_validate_template_with_type_field( + testdata_dir: pathlib.Path, tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch +) -> None: + """Test behaviour when validating an instance with x-trestle-template-type field.""" + task_template_folder = tmp_trestle_dir / '.trestle/author/test_task/' + test_template_folder = testdata_dir / 'author/governed_folders/template_folder_with_template_type' + test_instances_folder = testdata_dir / 'author/governed_folders/good_instance_with_template_type' + task_instance_folder = tmp_trestle_dir / 'test_task/folder_1' + + hidden_file = testdata_dir / pathlib.Path( + 'author/governed_folders/template_folder_with_drawio/.hidden_does_not_affect' + ) + test_utils.make_file_hidden(hidden_file) + + test_utils.copy_tree_or_file_with_hidden(test_template_folder, task_template_folder) + + shutil.copytree(test_instances_folder, task_instance_folder) + # test validate short + command_string_validate_content = 'trestle author folders validate -tn test_task -hv -vtt' + monkeypatch.setattr(sys, 'argv', command_string_validate_content.split()) + rc = trestle.cli.Trestle().run() + assert rc == 0 + + # test validate long + command_string_validate_content = 'trestle author folders validate -tn test_task -hv --validate-template-type' + monkeypatch.setattr(sys, 'argv', command_string_validate_content.split()) + rc = trestle.cli.Trestle().run() + assert rc == 0 + + +def test_validate_template_with_type_field_unhappy( + testdata_dir: pathlib.Path, tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch +) -> None: + """Test hunhappy behaviour when validating an instance with x-trestle-template-type field.""" + task_template_folder = tmp_trestle_dir / '.trestle/author/test_task/' + test_template_folder = testdata_dir / 'author/governed_folders/template_folder_with_template_type' + test_instances_folder = testdata_dir / 'author/governed_folders/good_instance_without_template_type' + task_instance_folder = tmp_trestle_dir / 'test_task/folder_1' + + hidden_file = testdata_dir / pathlib.Path( + 'author/governed_folders/template_folder_with_drawio/.hidden_does_not_affect' + ) + test_utils.make_file_hidden(hidden_file) + + test_utils.copy_tree_or_file_with_hidden(test_template_folder, task_template_folder) + + shutil.copytree(test_instances_folder, task_instance_folder) + # test validate short + command_string_validate_content = 'trestle author folders validate -tn test_task -hv -vtt' + monkeypatch.setattr(sys, 'argv', command_string_validate_content.split()) + rc = trestle.cli.Trestle().run() + assert rc == 1 + + # test validate long + command_string_validate_content = 'trestle author folders validate -tn test_task -hv --validate-template-type' + monkeypatch.setattr(sys, 'argv', command_string_validate_content.split()) + rc = trestle.cli.Trestle().run() + assert rc == 1 + + def test_heading_levels_hierarchy( testdata_dir: pathlib.Path, tmp_trestle_dir: pathlib.Path, monkeypatch: MonkeyPatch ) -> None: diff --git a/trestle/core/commands/author/consts.py b/trestle/core/commands/author/consts.py index b61d9cfff..61ccad7fd 100644 --- a/trestle/core/commands/author/consts.py +++ b/trestle/core/commands/author/consts.py @@ -47,9 +47,14 @@ 'Enable to validate README.md files. Required if readme files are included in the' + 'template.' ) +TEMPLATE_TYPE_VALIDATE_SHORT = '-vtt' +TEMPLATE_TYPE_VALIDATE_LONG = '--validate-template-type' +TEMPLATE_TYPE_VALIDATE_HELP = 'Validate that template and instance files match with x-trestle-template-type field' + START_TEMPLATE_VERSION = '0.0.1' # first ever template version, all templates without version will be defaulted to this TRESTLE_RESOURCES = 'trestle.resources' TEMPLATE_VERSION_HEADER = 'x-trestle-template-version' +TEMPLATE_TYPE_HEADER = 'x-trestle-template-type' # Governed heading - capability: To be removed GH_SHORT = '-gh' diff --git a/trestle/core/commands/author/folders.py b/trestle/core/commands/author/folders.py index 79f6843fd..4576c2a92 100644 --- a/trestle/core/commands/author/folders.py +++ b/trestle/core/commands/author/folders.py @@ -81,6 +81,13 @@ def _init_arguments(self) -> None: action='store_true' ) + self.add_argument( + author_const.TEMPLATE_TYPE_VALIDATE_SHORT, + author_const.TEMPLATE_TYPE_VALIDATE_LONG, + help=author_const.TEMPLATE_TYPE_VALIDATE_HELP, + action='store_true' + ) + def _run(self, args: argparse.Namespace) -> int: try: if self._initialize(args): @@ -102,7 +109,8 @@ def _run(self, args: argparse.Namespace) -> int: args.governed_heading, args.readme_validate, args.template_version, - args.ignore + args.ignore, + args.validate_template_type ) else: raise TrestleIncorrectArgsError(f'Unsupported mode: {args.mode} for folders command.') @@ -186,12 +194,13 @@ def _measure_template_folder( governed_heading: str, readme_validate: bool, template_version: str, - ignore: str + ignore: str, + validate_by_type_field: bool, ) -> bool: """ Validate instances against templates. - Validation will succeed iff: + Validation will succeed if: 1. All template files from the specified version are present in the task 2. All of the instances are valid """ @@ -217,8 +226,24 @@ def _measure_template_folder( if instance_file.suffix == const.MARKDOWN_FILE_EXT: md_api = MarkdownAPI() versioned_template_dir = None + # checks on naming template name out of type header if needed + if validate_by_type_field: + template_name = md_api.processor.fetch_value_from_header( + instance_file, author_const.TEMPLATE_TYPE_HEADER + ) + if template_name is None: + logger.warning( + f'INVALID: Instance file {instance_file_name} does not have' + f' {author_const.TEMPLATE_TYPE_HEADER}' + ' field in its header and can not be validated using optional parameter validate' + ' template type field' + ) + return False + template_name = template_name + '.md' + else: + template_name = instance_file_name if template_version != '': - template_file = self.template_dir / instance_file_name + template_file = self.template_dir / template_name versioned_template_dir = self.template_dir else: instance_version = md_api.processor.fetch_value_from_header( @@ -229,16 +254,29 @@ def _measure_template_folder( versioned_template_dir = TemplateVersioning.get_versioned_template_dir( self.template_dir, instance_version ) - template_file = versioned_template_dir / instance_file_name + template_file = versioned_template_dir / template_name # Check if instance is in the available templates, # additional files are allowed but should not be validated. templates = self._get_templates(versioned_template_dir, readme_validate) is_template_present = False + template_type_is_valid = False for template in templates: - if template.name == str(instance_file_name): - is_template_present = True - break + # checks if valdation needs to check on x-trestle-template-type field on header + if validate_by_type_field: + instance_template_type = md_api.processor.fetch_value_from_header( + instance_file, author_const.TEMPLATE_TYPE_HEADER + ) + if template.stem == instance_template_type: + is_template_present = True + template_type_is_valid = True + break + # validation through template type field is not needed and performs validation + # through file name flow as usual + else: + if template.name == str(instance_file_name): + is_template_present = True + break if not is_template_present: logger.info( @@ -252,7 +290,7 @@ def _measure_template_folder( [t.relative_to(versioned_template_dir) for t in templates], False ) - if instance_file_name in all_versioned_templates[instance_version]: + if instance_file_name in all_versioned_templates[instance_version] or template_type_is_valid: # validate md_api.load_validator_with_template( template_file, validate_header, not validate_only_header, governed_heading @@ -266,7 +304,13 @@ def _measure_template_folder( else: logger.info(f'VALID: {instance_file}') # mark template as present - all_versioned_templates[instance_version][instance_file_name] = True + if template_type_is_valid: + template_file_name = [ + temp.relative_to(versioned_template_dir) for temp in templates if temp.name == template_name + ] + all_versioned_templates[instance_version][template_file_name[0]] = True + else: + all_versioned_templates[instance_version][instance_file_name] = True elif instance_file.suffix == const.DRAWIO_FILE_EXT: drawio = draw_io.DrawIO(instance_file) @@ -359,7 +403,8 @@ def validate( governed_heading: str, readme_validate: bool, template_version: str, - ignore: str + ignore: str, + validate_by_type_field: bool, ) -> int: """Validate task.""" if not self.task_path.is_dir(): @@ -376,7 +421,8 @@ def validate( governed_heading, readme_validate, template_version, - ignore + ignore, + validate_by_type_field ) if not result: raise TrestleError(