diff --git a/.adr-dir b/.adr-dir index c73b64a..1b40ea6 100644 --- a/.adr-dir +++ b/.adr-dir @@ -1 +1 @@ -docs/adr +docs/adr_nygard_example diff --git a/README.md b/README.md index c0f5481..3bfd580 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,10 @@ This is a macro plugin to generates summaries from a list of a ADR documents in Examples and documentation can be found [here](https://febus982.github.io/mkdocs-macros-adr-summary) +The package should be stable enough for daily use. I'll release 1.0.0, and switch to semantic version, +as soon as support for MADR version 2 has been implemented. Until that breaking changes can be introduced +and will be documented in the GitHub release description. + ## Quick start Enable the plugin in `mkdocs.yml` @@ -31,20 +35,18 @@ Create a markdown page in your mkdocs website and use the `adr_summary` macro pr the path containing your ADR files relative to the `mkdocs.yml` file. ```markdown -# Summary - -{{ adr_summary(adr_path="docs/adr") }} +{{ adr_summary(adr_path="docs/adr", adr_style="nygard") }} ``` +`adr_style` can be `nygard` or `MADR3` + ## More customization The page output is generated using a jinja template, but you can provide a custom one. The file path must still be relative to the `mkdocs.yml` file. ```markdown -# Summary - -{{ adr_summary(adr_path="docs/adr", template_file="other.jinja") }} +{{ adr_summary(adr_path="docs/adr", adr_style="MADR3", template_file="other.jinja") }} ``` The default template is: @@ -65,23 +67,50 @@ The default template is: {% endfor %} ``` -The `document` variable in the jinja template is a Sequence of: +The `documents` variable in the jinja template is a Sequence of `ADRDocument` models: ```python @dataclass class ADRDocument: file_path: str - title: str - date: Optional[date] - statuses: Optional[Sequence[str]] + title: Optional[str] = None + date: Optional[date] = None + stasdetus: Optional[str] = None + statuses: Sequence[str] = tuple() + deciders: Optional[str] = None + consulted: Optional[str] = None + informed: Optional[str] = None ``` +There are some differences in what metadata is available when using different formats: + +| | Nygard | MADR3 | MADR2 | +|-----------|--------|-------|-------| +| file_path | ✅︎ | ✅︎ | ✅︎ | +| title | ✅︎ | ✅︎ | ✅︎ | +| date | ✅︎ | ✅︎ | TODO | +| status | ⚠ | ✅︎ | TODO | +| statuses | ✅︎ | ⚠ | TODO | +| deciders | ❌ | ✅︎ | TODO | +| consulted | ❌ | ✅︎ | TODO | +| informed | ❌ | ✅︎ | TODO | + +* **Nygard format** + * `status` is the last item `statuses`. (I don't believe we should use multiple + statuses, however `adr-tools` allows it) + * `deciders`, `consulted` and `informed` are not supported by the format +* **MADR3** + * I wasn't able to find an automated tool supporting superseding documents. + By looking at the template it looks like there's a single status. + `statuses` will return a list with a single status. + ## Supported ADR formats -The only supported ADR format currently is the `nygard` format, it is recommended to -use [adr-tools](https://github.com/npryce/adr-tools) to manage the directory. +The supported ADR formats are: +* `nygard` format, it is recommended to use [adr-tools](https://github.com/npryce/adr-tools) to manage the directory +* `MADR` [version 3](https://github.com/adr/madr/blob/3.0.0/template/adr-template.md) -Support for [MADR](https://adr.github.io/madr/) versions 2 and 3 will be added with future iterations. +Support for [MADR](https://adr.github.io/madr/) version 2 will be added in a future version. ## Commands for development diff --git a/docs/.pages b/docs/.pages index 09db7f9..8a6a992 100644 --- a/docs/.pages +++ b/docs/.pages @@ -1,3 +1,4 @@ nav: - Home: index.md - - ADR (Nygard format): adr + - ADR Example (Nygard format): adr_nygard_example + - ADR Example (MADR3 format): adr_madr3_example diff --git a/docs/adr/summary.md b/docs/adr/summary.md deleted file mode 100644 index 39fd060..0000000 --- a/docs/adr/summary.md +++ /dev/null @@ -1,3 +0,0 @@ -# Summary - -{{ adr_summary(adr_path="docs/adr") }} diff --git a/docs/adr_madr2_example/0001-record-architecture-decisions.md b/docs/adr_madr2_example/0001-record-architecture-decisions.md new file mode 100644 index 0000000..afa6224 --- /dev/null +++ b/docs/adr_madr2_example/0001-record-architecture-decisions.md @@ -0,0 +1,25 @@ +# Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 2.1.2 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 2.1.2", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. +* Version 2.1.2 is the latest one available when starting to document ADRs. diff --git a/docs/adr_madr2_example/template.md b/docs/adr_madr2_example/template.md new file mode 100644 index 0000000..4c7cf92 --- /dev/null +++ b/docs/adr_madr2_example/template.md @@ -0,0 +1,72 @@ +# [short title of solved problem and solution] source: https://github.com/adr/madr/tree/2.1.2/template + +* Status: [proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)] +* Deciders: [list everyone involved in the decision] +* Date: [YYYY-MM-DD when the decision was last updated] + +Technical Story: [description | ticket/issue URL] + +## Context and Problem Statement + +[Describe the context and problem statement, e.g., in free form using two to three sentences. You may want to articulate the problem in form of a question.] + +## Decision Drivers + +* [driver 1, e.g., a force, facing concern, …] +* [driver 2, e.g., a force, facing concern, …] +* … + +## Considered Options + +* [option 1] +* [option 2] +* [option 3] +* … + +## Decision Outcome + +Chosen option: "[option 1]", because [justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force force | … | comes out best (see below)]. + +### Positive Consequences + +* [e.g., improvement of quality attribute satisfaction, follow-up decisions required, …] +* … + +### Negative Consequences + +* [e.g., compromising quality attribute, follow-up decisions required, …] +* … + +## Pros and Cons of the Options + +### [option 1] + +[example | description | pointer to more information | …] + +* Good, because [argument a] +* Good, because [argument b] +* Bad, because [argument c] +* … + +### [option 2] + +[example | description | pointer to more information | …] + +* Good, because [argument a] +* Good, because [argument b] +* Bad, because [argument c] +* … + +### [option 3] + +[example | description | pointer to more information | …] + +* Good, because [argument a] +* Good, because [argument b] +* Bad, because [argument c] +* … + +## Links + +* [Link type] [Link to ADR] +* … \ No newline at end of file diff --git a/docs/adr_madr3_example/.markdownlint b/docs/adr_madr3_example/.markdownlint new file mode 100644 index 0000000..52b67b8 --- /dev/null +++ b/docs/adr_madr3_example/.markdownlint @@ -0,0 +1,20 @@ +# source: https://github.com/adr/madr/blob/3.0.0/template/.markdownlint.yml +default: true + +# Allow arbitrary line length +# +# Reason: We apply the one-sentence-per-line rule. A sentence may get longer than 80 characters, especially if links are contained. +# +# Details: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md013---line-length +MD013: false + +# Allow duplicate headings +# +# Reasons: +# +# - The chosen option is considerably often used as title of the ADR (e.g., ADR-0015). Thus, that title repeats. +# - We use "Examples" multiple times (e.g., ADR-0010). +# - Markdown lint should support the user and not annoy them. +# +# Details: https://github.com/DavidAnson/markdownlint/blob/main/doc/Rules.md#md024---multiple-headings-with-the-same-content +MD024: false \ No newline at end of file diff --git a/docs/adr/.pages b/docs/adr_madr3_example/.pages similarity index 100% rename from docs/adr/.pages rename to docs/adr_madr3_example/.pages diff --git a/docs/adr_madr3_example/0001-record-architecture-decisions.md b/docs/adr_madr3_example/0001-record-architecture-decisions.md new file mode 100644 index 0000000..bbe6557 --- /dev/null +++ b/docs/adr_madr3_example/0001-record-architecture-decisions.md @@ -0,0 +1,36 @@ +--- +# source: https://github.com/adr/madr/blob/3.0.0/template/adr-template.md +# These are optional elements. Feel free to remove any of them. +status: accepted +date: 2024-01-24 +# status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)} +# date: {YYYY-MM-DD when the decision was last updated} +# deciders: {list everyone involved in the decision} +# consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication} +# informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication} +--- +# Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 3.0.0 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 3.0.0", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* MADR allows for structured capturing of any decision. +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. diff --git a/docs/adr_madr3_example/adr-template.md b/docs/adr_madr3_example/adr-template.md new file mode 100644 index 0000000..8c12b89 --- /dev/null +++ b/docs/adr_madr3_example/adr-template.md @@ -0,0 +1,80 @@ +--- +# source: https://github.com/adr/madr/blob/3.0.0/template/adr-template.md +# These are optional elements. Feel free to remove any of them. +status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)} +date: {YYYY-MM-DD when the decision was last updated} +deciders: {list everyone involved in the decision} +consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication} +informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication} +--- +# {short title of solved problem and solution} + +## Context and Problem Statement + +{Describe the context and problem statement, e.g., in free form using two to three sentences or in the form of an illustrative story. + You may want to articulate the problem in form of a question and add links to collaboration boards or issue management systems.} + + +## Decision Drivers + +* {decision driver 1, e.g., a force, facing concern, …} +* {decision driver 2, e.g., a force, facing concern, …} +* … + +## Considered Options + +* {title of option 1} +* {title of option 2} +* {title of option 3} +* … + +## Decision Outcome + +Chosen option: "{title of option 1}", because +{justification. e.g., only option, which meets k.o. criterion decision driver | which resolves force {force} | … | comes out best (see below)}. + + +### Consequences + +* Good, because {positive consequence, e.g., improvement of one or more desired qualities, …} +* Bad, because {negative consequence, e.g., compromising one or more desired qualities, …} +* … + + +## Validation + +{describe how the implementation of/compliance with the ADR is validated. E.g., by a review or an ArchUnit test} + + +## Pros and Cons of the Options + +### {title of option 1} + + +{example | description | pointer to more information | …} + +* Good, because {argument a} +* Good, because {argument b} + +* Neutral, because {argument c} +* Bad, because {argument d} +* … + +### {title of other option} + +{example | description | pointer to more information | …} + +* Good, because {argument a} +* Good, because {argument b} +* Neutral, because {argument c} +* Bad, because {argument d} +* … + + +## More Information + +{You might want to provide additional evidence/confidence for the decision outcome here and/or + document the team agreement on the decision and/or + define when this decision when and how the decision should be realized and if/when it should be re-visited and/or + how the decision is validated. + Links to other decisions and resources might here appear as well.} diff --git a/docs/adr_madr3_example/summary.md b/docs/adr_madr3_example/summary.md new file mode 100644 index 0000000..b724c0f --- /dev/null +++ b/docs/adr_madr3_example/summary.md @@ -0,0 +1,3 @@ +# Summary + +{{ adr_summary(adr_path="docs/adr_madr3_example", adr_style="MADR3") }} diff --git a/docs/adr_nygard_example/.pages b/docs/adr_nygard_example/.pages new file mode 100644 index 0000000..de2d73a --- /dev/null +++ b/docs/adr_nygard_example/.pages @@ -0,0 +1,5 @@ +order: desc + +nav: + - summary.md + - ... diff --git a/docs/adr/0001-record-architecture-decisions.md b/docs/adr_nygard_example/0001-record-architecture-decisions.md similarity index 100% rename from docs/adr/0001-record-architecture-decisions.md rename to docs/adr_nygard_example/0001-record-architecture-decisions.md diff --git a/docs/adr/0002-supercedes-1.md b/docs/adr_nygard_example/0002-supercedes-1.md similarity index 100% rename from docs/adr/0002-supercedes-1.md rename to docs/adr_nygard_example/0002-supercedes-1.md diff --git a/docs/adr/0003-supercedes-1-b.md b/docs/adr_nygard_example/0003-supercedes-1-b.md similarity index 100% rename from docs/adr/0003-supercedes-1-b.md rename to docs/adr_nygard_example/0003-supercedes-1-b.md diff --git a/docs/adr_nygard_example/summary.md b/docs/adr_nygard_example/summary.md new file mode 100644 index 0000000..65d9c1a --- /dev/null +++ b/docs/adr_nygard_example/summary.md @@ -0,0 +1,3 @@ +# Summary + +{{ adr_summary(adr_path="docs/adr_nygard_example", adr_style="nygard") }} diff --git a/docs/index.md b/docs/index.md index d6992c9..b99e48f 100644 --- a/docs/index.md +++ b/docs/index.md @@ -18,6 +18,10 @@ This is a macro plugin to generates summaries from a list of a ADR documents in Examples and documentation can be found [here](https://febus982.github.io/mkdocs-macros-adr-summary) +The package should be stable enough for daily use. I'll release 1.0.0, and switch to semantic version, +as soon as support for MADR version 2 has been implemented. Until that breaking changes can be introduced +and will be documented in the GitHub release description. + ## Quick start Enable the plugin in `mkdocs.yml` @@ -33,10 +37,12 @@ the path containing your ADR files relative to the `mkdocs.yml` file. ``` {% raw %} -{{ adr_summary(adr_path="docs/adr") }} +{{ adr_summary(adr_path="docs/adr", adr_style="nygard") }} {% endraw %} ``` +`adr_style` can be `nygard` or `MADR3` + ## More customization The page output is generated using a jinja template, but you can provide a custom one. The file path @@ -44,7 +50,7 @@ must still be relative to the `mkdocs.yml` file. ``` {% raw %} -{{ adr_summary(adr_path="docs/adr", template_file="other.jinja") }} +{{ adr_summary(adr_path="docs/adr", adr_style="MADR3", template_file="other.jinja") }} {% endraw %} ``` @@ -68,23 +74,50 @@ The default template is: {% endraw %} ``` -The `document` variable in the jinja template is a Sequence of: +The `documents` variable in the jinja template is a Sequence of `ADRDocument` models: ```python @dataclass class ADRDocument: file_path: str - title: str - date: Optional[date] - statuses: Optional[Sequence[str]] + title: Optional[str] = None + date: Optional[date] = None + stasdetus: Optional[str] = None + statuses: Sequence[str] = tuple() + deciders: Optional[str] = None + consulted: Optional[str] = None + informed: Optional[str] = None ``` +There are some differences in what metadata is available when using different formats: + +| | Nygard | MADR3 | MADR2 | +|-----------|--------|-------|-------| +| file_path | ✅︎ | ✅︎ | ✅︎ | +| title | ✅︎ | ✅︎ | ✅︎ | +| date | ✅︎ | ✅︎ | TODO | +| status | ⚠ | ✅︎ | TODO | +| statuses | ✅︎ | ⚠ | TODO | +| deciders | ❌ | ✅︎ | TODO | +| consulted | ❌ | ✅︎ | TODO | +| informed | ❌ | ✅︎ | TODO | + +* **Nygard format** + * `status` is the last item `statuses`. (I don't believe we should use multiple + statuses, however `adr-tools` allows it) + * `deciders`, `consulted` and `informed` are not supported by the format +* **MADR3** + * I wasn't able to find an automated tool supporting superseding documents. + By looking at the template it looks like there's a single status. + `statuses` will return a list with a single status. + ## Supported ADR formats -The only supported ADR format currently is the `nygard` format, it is recommended to -use [adr-tools](https://github.com/npryce/adr-tools) to manage the directory. +The supported ADR formats are: +* `nygard` format, it is recommended to use [adr-tools](https://github.com/npryce/adr-tools) to manage the directory +* `MADR` [version 3](https://github.com/adr/madr/blob/3.0.0/template/adr-template.md) -Support for [MADR](https://adr.github.io/madr/) versions 2 and 3 will be added with future iterations. +Support for [MADR](https://adr.github.io/madr/) version 2 will be added in a future version. ## Commands for development diff --git a/mkdocs_macros_adr_summary/factory.py b/mkdocs_macros_adr_summary/factory.py index 878f60f..f00a1f9 100644 --- a/mkdocs_macros_adr_summary/factory.py +++ b/mkdocs_macros_adr_summary/factory.py @@ -1,12 +1,15 @@ from typing import Dict, Type -from .interfaces import ADRParser, ADRStyle -from .parser import NygardParser +from .interfaces import ADRParser, TYPE_ADRStyle +from .parser import MADR3Parser, NygardParser -parser_registry: Dict[ADRStyle, Type[ADRParser]] = {"nygard": NygardParser} +parser_registry: Dict[TYPE_ADRStyle, Type[ADRParser]] = { + "nygard": NygardParser, + "MADR3": MADR3Parser, +} -def get_parser(adr_style: ADRStyle) -> Type[ADRParser]: +def get_parser(adr_style: TYPE_ADRStyle) -> Type[ADRParser]: try: parser = parser_registry[adr_style] except KeyError: diff --git a/mkdocs_macros_adr_summary/interfaces.py b/mkdocs_macros_adr_summary/interfaces.py index 11934cf..3cdf2fc 100644 --- a/mkdocs_macros_adr_summary/interfaces.py +++ b/mkdocs_macros_adr_summary/interfaces.py @@ -1,27 +1,25 @@ from abc import ABC, abstractmethod from dataclasses import dataclass from datetime import date -from enum import Enum from pathlib import Path from typing import Literal, Optional, Sequence -ADRStyle = Literal["nygard"] - - -class ADRFormat(Enum): - nygard = 1 +TYPE_ADRStyle = Literal["MADR2", "MADR3", "nygard"] @dataclass class ADRDocument: file_path: str - title: str - date: Optional[date] - statuses: Optional[Sequence[str]] + title: Optional[str] = None + date: Optional[date] = None + status: Optional[str] = None + statuses: Sequence[str] = tuple() + deciders: Optional[str] = None + consulted: Optional[str] = None + informed: Optional[str] = None class ADRParser(ABC): @staticmethod @abstractmethod - def parse(file_path: Path, base_path: Path) -> ADRDocument: - ... + def parse(file_path: Path, base_path: Path) -> ADRDocument: ... diff --git a/mkdocs_macros_adr_summary/parser.py b/mkdocs_macros_adr_summary/parser.py deleted file mode 100644 index 93d83b2..0000000 --- a/mkdocs_macros_adr_summary/parser.py +++ /dev/null @@ -1,82 +0,0 @@ -from datetime import date, datetime -from pathlib import Path -from typing import List, Optional, Tuple - -from mistune import BlockState, Markdown, create_markdown -from mistune.renderers.markdown import MarkdownRenderer -from mkdocs_macros import fix_url - -from mkdocs_macros_adr_summary.interfaces import ADRDocument, ADRParser - -AST_TYPE = Tuple[List, BlockState] - - -class NygardParser(ADRParser): - renderer = MarkdownRenderer() - parser: Markdown = create_markdown( - escape=False, - renderer=None, - plugins=["strikethrough", "footnotes", "table", "speedup"], - ) - - @classmethod - def parse(cls, file_path: Path, base_path: Path) -> ADRDocument: - with open(file_path, "r") as f: - md_ast = cls.parser.parse(f.read()) - - doc = ADRDocument( - file_path=fix_url(str(file_path.relative_to(base_path))), - title=cls._get_title(md_ast) or "==INVALID_TITLE==", - date=cls._get_datetime(md_ast), - statuses=cls._get_statuses(md_ast) or ("==INVALID_STATUS==",), - ) - return doc - - @classmethod - def _get_title(cls, document: AST_TYPE) -> Optional[str]: - # There can be no document without at least the first line - block = document[0][0] - - if ( - not block.get("type") == "heading" - or block.get("attrs", {}).get("level") != 1 - ): - return None - - return cls.renderer.paragraph(block, document[1]).strip() - - @classmethod - def _get_datetime(cls, document: AST_TYPE) -> Optional[date]: - try: - block = document[0][2] - except IndexError: - return None - - if not block.get("type") == "paragraph": - return None - - raw_text = cls.renderer.paragraph(block, document[1]).strip() - try: - return datetime.strptime(raw_text, "Date: %Y-%m-%d").date() - except ValueError: - return None - - @classmethod - def _get_statuses(cls, document: AST_TYPE) -> Tuple[str, ...]: - statuses: List[str] = [] - - i = 6 - while i < len(document[0]): - try: - block = document[0][i] - except IndexError: - break - - if block.get("type") == "paragraph": - statuses.append(cls.renderer.paragraph(block, document[1]).strip()) - else: - break - - i += 2 - - return tuple(statuses) diff --git a/mkdocs_macros_adr_summary/parser/__init__.py b/mkdocs_macros_adr_summary/parser/__init__.py new file mode 100644 index 0000000..e65f608 --- /dev/null +++ b/mkdocs_macros_adr_summary/parser/__init__.py @@ -0,0 +1,2 @@ +from .madr3 import MADR3Parser +from .nygard import NygardParser diff --git a/mkdocs_macros_adr_summary/parser/base.py b/mkdocs_macros_adr_summary/parser/base.py new file mode 100644 index 0000000..47e1496 --- /dev/null +++ b/mkdocs_macros_adr_summary/parser/base.py @@ -0,0 +1,88 @@ +from abc import ABC, abstractmethod +from datetime import date +from pathlib import Path +from typing import Any, Dict, Optional, Sequence, Tuple + +from mistune import Markdown, create_markdown +from mistune.renderers.markdown import MarkdownRenderer +from mkdocs_macros import fix_url + +from mkdocs_macros_adr_summary.interfaces import ADRDocument, ADRParser + +from .exceptions import InvalidFileError +from .types import TYPE_AST + + +class BaseParser(ADRParser, ABC): + renderer: MarkdownRenderer = MarkdownRenderer() + parser: Markdown = create_markdown( + escape=False, + renderer=None, + plugins=["strikethrough", "footnotes", "table", "speedup"], + ) + + @classmethod + def parse(cls, file_path: Path, base_path: Path) -> ADRDocument: + with open(file_path, "r") as f: + file = f.read() + try: + metadata, ast = cls._get_metadata_and_ast(file) + except LookupError as e: + raise InvalidFileError(file_path) from e + + doc = ADRDocument( + file_path=fix_url(str(file_path.relative_to(base_path))), + title=cls._get_title(metadata, ast), + date=cls._get_date(metadata, ast), + status=cls._get_status(metadata, ast), + statuses=cls._get_statuses(metadata, ast), + deciders=cls._get_deciders(metadata, ast), + consulted=cls._get_consulted(metadata, ast), + informed=cls._get_informed(metadata, ast), + ) + + return doc + + @classmethod + def _get_title(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + h1_list = [ + i + for i, x in enumerate(ast[0]) + if x.get("type") == "heading" and x.get("attrs", {}).get("level") == 1 + ] + if len(h1_list) != 1: + return None + + return cls.renderer.paragraph(ast[0][h1_list[0]], ast[1]).strip() + + @classmethod + @abstractmethod + def _get_date(cls, metadata: dict, ast: TYPE_AST) -> Optional[date]: ... + + @classmethod + @abstractmethod + def _get_statuses(cls, metadata: dict, ast: TYPE_AST) -> Sequence[str]: ... + + @classmethod + @abstractmethod + def _get_status(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: ... + + @classmethod + @abstractmethod + def _get_metadata_and_ast(cls, file: str) -> Tuple[Dict[str, Any], TYPE_AST]: ... + + @classmethod + @abstractmethod + def _get_deciders(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: ... + + @classmethod + @abstractmethod + def _get_consulted(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: ... + + @classmethod + @abstractmethod + def _get_informed(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: ... + + @staticmethod + def _upperfirst(text: str) -> str: + return text[0].capitalize() + text[1::] if text[0].islower() else text diff --git a/mkdocs_macros_adr_summary/parser/exceptions.py b/mkdocs_macros_adr_summary/parser/exceptions.py new file mode 100644 index 0000000..12ce38d --- /dev/null +++ b/mkdocs_macros_adr_summary/parser/exceptions.py @@ -0,0 +1,9 @@ +from pathlib import Path + + +class InvalidFileError(Exception): + def __init__(self, file_path: Path, *args, **kwargs): + super().__init__(*args, **kwargs) + self.file_path = file_path + + file_path: Path diff --git a/mkdocs_macros_adr_summary/parser/madr3.py b/mkdocs_macros_adr_summary/parser/madr3.py new file mode 100644 index 0000000..a62f266 --- /dev/null +++ b/mkdocs_macros_adr_summary/parser/madr3.py @@ -0,0 +1,61 @@ +from datetime import date +from typing import Any, Dict, Optional, Sequence, Tuple + +from yaml import safe_load +from yaml.scanner import ScannerError + +from .base import BaseParser +from .types import TYPE_AST + + +class MADR3Parser(BaseParser): + @classmethod + def _get_metadata_and_ast(cls, file: str) -> Tuple[Dict[str, Any], TYPE_AST]: + lines = file.splitlines() + separators = [i for i, x in enumerate(lines) if x == "---"] + if len(separators) < 2: + raise LookupError("Metadata section not found in file") + + yaml_file = lines[0 : separators[1] :] + try: + metadata: Optional[Dict[str, str]] = safe_load("\n".join(yaml_file)) + except ScannerError: + raise LookupError("Cannot parse metadata section") + + md_file = lines[separators[1] + 1 : :] + md_ast = cls.parser.parse("\n".join(md_file)) + + return metadata or {}, md_ast + + @classmethod + def _get_date(cls, metadata: dict, ast: TYPE_AST) -> Optional[date]: + if isinstance(metadata.get("date"), date): + return metadata["date"] + else: + return None + + @classmethod + def _get_statuses(cls, metadata: dict, ast: TYPE_AST) -> Sequence[str]: + if metadata.get("status"): + return (cls._upperfirst(metadata["status"]),) + else: + return tuple() + + @classmethod + def _get_status(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + try: + return cls._get_statuses(metadata, ast)[0] + except IndexError: + return None + + @classmethod + def _get_deciders(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + return metadata.get("deciders") + + @classmethod + def _get_consulted(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + return metadata.get("consulted") + + @classmethod + def _get_informed(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + return metadata.get("informed") diff --git a/mkdocs_macros_adr_summary/parser/nygard.py b/mkdocs_macros_adr_summary/parser/nygard.py new file mode 100644 index 0000000..ab2f4e3 --- /dev/null +++ b/mkdocs_macros_adr_summary/parser/nygard.py @@ -0,0 +1,83 @@ +from datetime import date, datetime +from typing import Any, Dict, List, Optional, Sequence, Tuple + +from .base import BaseParser +from .types import TYPE_AST + + +class NygardParser(BaseParser): + @classmethod + def _get_metadata_and_ast(cls, file: str) -> Tuple[Dict[str, Any], TYPE_AST]: + md_ast = cls.parser.parse(file) + return {}, md_ast + + @classmethod + def _get_date(cls, metadata: dict, document: TYPE_AST) -> Optional[date]: + h1_list = [ + i + for i, x in enumerate(document[0]) + if x.get("type") == "heading" and x.get("attrs", {}).get("level") == 1 + ] + if len(h1_list) != 1: + return None + + try: + block = document[0][h1_list[0] + 2] + except IndexError: + return None + + if not block.get("type") == "paragraph": + return None + + raw_text = cls.renderer.paragraph(block, document[1]).strip() + try: + return datetime.strptime(raw_text, "Date: %Y-%m-%d").date() + except ValueError: + return None + + @classmethod + def _get_statuses(cls, metadata: dict, document: TYPE_AST) -> Sequence[str]: + statuses: List[str] = [] + + # Find status header + h2_list = [ + i + for i, x in enumerate(document[0]) + if x.get("type") == "heading" + and x.get("attrs", {}).get("level") == 2 + and cls.renderer.paragraph(x, document[1]).strip() == "Status" + ] + if len(h2_list) != 1: + return tuple(statuses) + + i = h2_list[0] + 1 + while i < len(document[0]): + block = document[0][i] + + if block.get("type") == "paragraph": + statuses.append(cls.renderer.paragraph(block, document[1]).strip()) + i += 1 + elif block.get("type") == "heading": + break + else: + i += 1 + continue + + return tuple(statuses) + + @classmethod + def _get_status(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + statuses = cls._get_statuses(metadata, ast) + return statuses[-1] if statuses else None + + @classmethod + def _get_deciders(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + return None + + @classmethod + def _get_consulted(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + return None + + @classmethod + def _get_informed(cls, metadata: dict, ast: TYPE_AST) -> Optional[str]: + return None diff --git a/mkdocs_macros_adr_summary/parser/types.py b/mkdocs_macros_adr_summary/parser/types.py new file mode 100644 index 0000000..877667c --- /dev/null +++ b/mkdocs_macros_adr_summary/parser/types.py @@ -0,0 +1,5 @@ +from typing import List, Tuple + +from mistune import BlockState + +TYPE_AST = Tuple[List, BlockState] diff --git a/mkdocs_macros_adr_summary/plugin.py b/mkdocs_macros_adr_summary/plugin.py index 296907e..5f5fc50 100644 --- a/mkdocs_macros_adr_summary/plugin.py +++ b/mkdocs_macros_adr_summary/plugin.py @@ -6,7 +6,7 @@ from mkdocs_macros.plugin import MacrosPlugin from .factory import get_parser -from .interfaces import ADRStyle +from .interfaces import TYPE_ADRStyle from .renderer import Jinja2Renderer ADR_REGEX = re.compile("^[0-9]{4}-[\\w-]+\\.md*") @@ -15,7 +15,7 @@ def adr_summary( env: MacrosPlugin, adr_path: str, - adr_style: ADRStyle = "nygard", + adr_style: TYPE_ADRStyle, template_file: Optional[str] = None, ) -> str: absolute_path = Path(env.project_dir).joinpath(adr_path) diff --git a/mkdocs_macros_adr_summary/template.jinja b/mkdocs_macros_adr_summary/template.jinja index 83b873d..0c8a0ad 100644 --- a/mkdocs_macros_adr_summary/template.jinja +++ b/mkdocs_macros_adr_summary/template.jinja @@ -2,7 +2,7 @@ {% for d in documents %} * [{{ d.title }}]({{ d.filename }}) - * `{{ d.date.strftime('%d-%m-%Y') }}` + * `{{ d.date.strftime('%d-%m-%Y') if d.date }}` * `{{ d.file_path }}` {% if d.statuses %} * Statuses: diff --git a/pyproject.toml b/pyproject.toml index f423360..b0612db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,6 +52,7 @@ mistune = "^3.0.2" mkdocs = "^1.4.3" mkdocs-macros-plugin = "^1.0.5" python = ">=3.8,<3.13" +pyyaml = "^6.0.1" [tool.poetry.group.dev] optional = true @@ -71,6 +72,7 @@ pytest-cov = ">=4.0.0" pytest-factoryboy = ">=2.5.0" pytest-xdist = ">=3.0.2" ruff = ">=0.0.263" +types-pyyaml = "^6.0.12.12" [tool.pytest.ini_options] asyncio_mode = "auto" @@ -111,12 +113,7 @@ extend-exclude = ["docs"] "__init__.py" = ["F401"] [tool.black] -files = ''' -( - mkdocs_macros_adr_summary - tests -) -''' +target-version = ["py38", "py39", "py310", "py311", "py312"] extend-exclude = ''' ( /docs diff --git a/tests/adr_docs/madr3/invalid_blank_document.md b/tests/adr_docs/madr3/invalid_blank_document.md new file mode 100644 index 0000000..e69de29 diff --git a/tests/adr_docs/madr3/invalid_headers.md b/tests/adr_docs/madr3/invalid_headers.md new file mode 100644 index 0000000..7d9c7a1 --- /dev/null +++ b/tests/adr_docs/madr3/invalid_headers.md @@ -0,0 +1,37 @@ +--- +#source: https://github.com/adr/madr/blob/3.0.0/template/adr-template.md +#These are optional elements. Feel free to remove any of them. +status: accepted +date: 2024-01-20 +Someinvalidstringhere +#status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)} +#date: {YYYY-MM-DD when the decision was last updated} +#deciders: {list everyone involved in the decision} +#consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication} +#informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication} +--- +Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 3.0.0 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 3.0.0", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* MADR allows for structured capturing of any decision. +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. diff --git a/tests/adr_docs/madr3/invalid_title_h3.md b/tests/adr_docs/madr3/invalid_title_h3.md new file mode 100644 index 0000000..07f2ebb --- /dev/null +++ b/tests/adr_docs/madr3/invalid_title_h3.md @@ -0,0 +1,36 @@ +--- +#source: https://github.com/adr/madr/blob/3.0.0/template/adr-template.md +#These are optional elements. Feel free to remove any of them. +status: accepted +date: 2024-01-20 +#status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)} +#date: {YYYY-MM-DD when the decision was last updated} +#deciders: {list everyone involved in the decision} +#consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication} +#informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication} +--- +### Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 3.0.0 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 3.0.0", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* MADR allows for structured capturing of any decision. +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. diff --git a/tests/adr_docs/madr3/invalid_title_p.md b/tests/adr_docs/madr3/invalid_title_p.md new file mode 100644 index 0000000..9a6e7c5 --- /dev/null +++ b/tests/adr_docs/madr3/invalid_title_p.md @@ -0,0 +1,36 @@ +--- +#source: https://github.com/adr/madr/blob/3.0.0/template/adr-template.md +#These are optional elements. Feel free to remove any of them. +status: accepted +date: 2024-01-20 +#status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)} +#date: {YYYY-MM-DD when the decision was last updated} +#deciders: {list everyone involved in the decision} +#consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication} +#informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication} +--- +Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 3.0.0 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 3.0.0", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* MADR allows for structured capturing of any decision. +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. diff --git a/tests/adr_docs/madr3/valid_with_metadata.md b/tests/adr_docs/madr3/valid_with_metadata.md new file mode 100644 index 0000000..89d8fb6 --- /dev/null +++ b/tests/adr_docs/madr3/valid_with_metadata.md @@ -0,0 +1,34 @@ +--- +#source: https://github.com/adr/madr/blob/3.0.0/template/adr-template.md +#These are optional elements. Feel free to remove any of them. +status: accepted +date: 2024-01-20 +deciders: Nick Fury +consulted: Anthony Stark +informed: Thor Odinson +--- +# Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 3.0.0 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 3.0.0", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* MADR allows for structured capturing of any decision. +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. diff --git a/tests/adr_docs/madr3/valid_without_metadata.md b/tests/adr_docs/madr3/valid_without_metadata.md new file mode 100644 index 0000000..3829274 --- /dev/null +++ b/tests/adr_docs/madr3/valid_without_metadata.md @@ -0,0 +1,36 @@ +--- +#source: https://github.com/adr/madr/blob/3.0.0/template/adr-template.md +#These are optional elements. Feel free to remove any of them. +#status: accepted +#date: 2024-01-20 +#status: {proposed | rejected | accepted | deprecated | … | superseded by [ADR-0005](0005-example.md)} +#date: {YYYY-MM-DD when the decision was last updated} +#deciders: {list everyone involved in the decision} +#consulted: {list everyone whose opinions are sought (typically subject-matter experts); and with whom there is a two-way communication} +#informed: {list everyone who is kept up-to-date on progress; and with whom there is a one-way communication} +--- +# Use Markdown Any Decision Records + +## Context and Problem Statement + +We want to record any decisions made in this project independent whether decisions concern the architecture ("architectural decision record"), the code, or other fields. +Which format and structure should these records follow? + +## Considered Options + +* [MADR](https://adr.github.io/madr/) 3.0.0 – The Markdown Any Decision Records +* [Michael Nygard's template](http://thinkrelevance.com/blog/2011/11/15/documenting-architecture-decisions) – The first incarnation of the term "ADR" +* Other templates listed at +* Formless – No conventions for file format and structure + +## Decision Outcome + +Chosen option: "MADR 3.0.0", because + +* Implicit assumptions should be made explicit. + Design documentation is important to enable people understanding the decisions later on. + See also [A rational design process: How and why to fake it](https://doi.org/10.1109/TSE.1986.6312940). +* MADR allows for structured capturing of any decision. +* The MADR format is lean and fits our development style. +* The MADR structure is comprehensible and facilitates usage & maintenance. +* The MADR project is vivid. diff --git a/tests/adr_docs/nygard/invalid_date_broken_file.md b/tests/adr_docs/nygard/invalid_date_broken_file.md new file mode 100644 index 0000000..cdaf8c2 --- /dev/null +++ b/tests/adr_docs/nygard/invalid_date_broken_file.md @@ -0,0 +1 @@ +# 1. Record architecture decisions diff --git a/tests/adr_docs/nygard/invalid_lines_delta.md b/tests/adr_docs/nygard/invalid_lines_delta.md index 87734df..5cb1c8c 100644 --- a/tests/adr_docs/nygard/invalid_lines_delta.md +++ b/tests/adr_docs/nygard/invalid_lines_delta.md @@ -1,5 +1,5 @@ -# 1. Record architecture decisions (NOT H1) +# 1. Record architecture decisions Date: 2024-01-20 diff --git a/tests/adr_docs/nygard/invalid_status_broken_file.md b/tests/adr_docs/nygard/invalid_status_broken_file.md new file mode 100644 index 0000000..7cdd229 --- /dev/null +++ b/tests/adr_docs/nygard/invalid_status_broken_file.md @@ -0,0 +1,5 @@ +# 1. Record architecture decisions + +Date: 2024-01-20 + +## Status diff --git a/tests/adr_docs/nygard/invalid_status_broken_file2.md b/tests/adr_docs/nygard/invalid_status_broken_file2.md new file mode 100644 index 0000000..095dfe7 --- /dev/null +++ b/tests/adr_docs/nygard/invalid_status_broken_file2.md @@ -0,0 +1 @@ +## Status \ No newline at end of file diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..ae50411 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,13 @@ +import factory +from pytest_factoryboy import register + +from mkdocs_macros_adr_summary.interfaces import ADRDocument + + +# This generates `adr_document_factory` and `adr_document` fixtures +@register +class ADRDocumentFactory(factory.Factory): + class Meta: + model = ADRDocument + + file_path: str = "../adr_docs/madr3/some-architectural-decision.md" diff --git a/tests/test_madr3_parser.py b/tests/test_madr3_parser.py new file mode 100644 index 0000000..9b405b3 --- /dev/null +++ b/tests/test_madr3_parser.py @@ -0,0 +1,75 @@ +from datetime import datetime +from pathlib import Path + +import pytest + +from mkdocs_macros_adr_summary.parser.exceptions import InvalidFileError +from mkdocs_macros_adr_summary.parser.madr3 import MADR3Parser + + +@pytest.mark.parametrize( + ["filename", "expected_metadata"], + [ + ( + "valid_with_metadata.md", + { + "status": "Accepted", + "statuses": tuple(["Accepted"]), + "date": datetime.fromisoformat("2024-01-20").date(), + "deciders": "Nick Fury", + "consulted": "Anthony Stark", + "informed": "Thor Odinson", + }, + ), + ( + "valid_without_metadata.md", + { + "status": None, + "statuses": tuple(), + "date": None, + "deciders": None, + "consulted": None, + "informed": None, + }, + ), + ], +) +def test_parse_valid_document( + filename: str, expected_metadata: dict, adr_document_factory +): + assert MADR3Parser.parse( + Path(__file__).parent.joinpath(f"adr_docs/madr3/{filename}"), + base_path=Path(__file__).parent, + ) == adr_document_factory( + file_path=f"../adr_docs/madr3/{filename}", + title="Use Markdown Any Decision Records", + **expected_metadata, + ) + + +def test_parse_invalid_blank_document(): + with pytest.raises(InvalidFileError): + MADR3Parser.parse( + Path(__file__).parent.joinpath("adr_docs/madr3/invalid_blank_document.md"), + base_path=Path(__file__).parent, + ) + + +@pytest.mark.parametrize( + ["filename"], + [("invalid_title_h3.md",), ("invalid_title_p.md",)], +) +def test_parse_invalid_title(filename: str, adr_document_factory): + document = MADR3Parser.parse( + Path(__file__).parent.joinpath(f"adr_docs/madr3/{filename}"), + base_path=Path(__file__).parent, + ) + assert document.title == adr_document_factory().title + + +def test_parse_invalid_headers(): + with pytest.raises(InvalidFileError): + MADR3Parser.parse( + Path(__file__).parent.joinpath("adr_docs/madr3/invalid_headers.md"), + base_path=Path(__file__).parent, + ) diff --git a/tests/test_nygard_parser.py b/tests/test_nygard_parser.py index 4aa9734..bb447ba 100644 --- a/tests/test_nygard_parser.py +++ b/tests/test_nygard_parser.py @@ -3,59 +3,66 @@ import pytest -from mkdocs_macros_adr_summary.interfaces import ADRDocument from mkdocs_macros_adr_summary.parser import NygardParser @pytest.mark.parametrize( ["filename", "expected_statuses"], [ - ("valid.md", tuple(["Accepted"])), + ( + "valid.md", + ("Accepted",), + ), ( "valid_multi_status.md", - tuple( - [ - "Accepted", - "Supercedes [1. Record architecture decisions]" - "(0001-record-architecture-decisions.md)", - ] + ( + "Accepted", + "Supercedes [1. Record architecture decisions]" + "(0001-record-architecture-decisions.md)", ), ), ], ) -def test_parse_valid_document(filename: str, expected_statuses: tuple): - assert NygardParser.parse( +def test_parse_valid_document( + filename: str, expected_statuses: tuple, adr_document_factory +) -> None: + doc = NygardParser.parse( Path(__file__).parent.joinpath(f"adr_docs/nygard/{filename}"), base_path=Path(__file__).parent, - ) == ADRDocument( + ) + doc2 = adr_document_factory( file_path=f"../adr_docs/nygard/{filename}", title="1. Record architecture decisions", date=datetime.fromisoformat("2024-01-20").date(), + status=expected_statuses[-1], statuses=expected_statuses, ) + assert doc == doc2 -def test_parse_invalid_lines_delta(): +def test_can_parse_invalid_lines_delta(adr_document_factory): assert NygardParser.parse( Path(__file__).parent.joinpath("adr_docs/nygard/invalid_lines_delta.md"), base_path=Path(__file__).parent, - ) == ADRDocument( + ) == adr_document_factory( file_path="../adr_docs/nygard/invalid_lines_delta.md", - title="==INVALID_TITLE==", - date=None, - statuses=tuple(["==INVALID_STATUS=="]), + title="1. Record architecture decisions", + date=datetime.fromisoformat("2024-01-20").date(), + status="Accepted", + statuses=("Accepted",), ) -def test_parse_invalid_blank_document(): +def test_parse_invalid_blank_document(adr_document_factory): assert NygardParser.parse( Path(__file__).parent.joinpath("adr_docs/nygard/invalid_blank_document.md"), base_path=Path(__file__).parent, - ) == ADRDocument( + ) == adr_document_factory( file_path="../adr_docs/nygard/invalid_blank_document.md", - title="==INVALID_TITLE==", + title=None, date=None, - statuses=tuple(["==INVALID_STATUS=="]), + status=None, + statuses=tuple(), ) @@ -63,45 +70,53 @@ def test_parse_invalid_blank_document(): ["filename"], [("invalid_title_h3.md",), ("invalid_title_p.md",)], ) -def test_parse_invalid_title(filename: str): - assert NygardParser.parse( +def test_parse_invalid_title(filename: str, adr_document_factory): + document = NygardParser.parse( Path(__file__).parent.joinpath(f"adr_docs/nygard/{filename}"), base_path=Path(__file__).parent, - ) == ADRDocument( - file_path=f"../adr_docs/nygard/{filename}", - title="==INVALID_TITLE==", - date=datetime.fromisoformat("2024-01-20").date(), - statuses=tuple(["Accepted"]), + ) + assert ( + document.title + == adr_document_factory( + title=None, + ).title ) @pytest.mark.parametrize( ["filename"], - [("invalid_date_h3.md",), ("invalid_date_format.md",), ("invalid_date_blank.md",)], + [ + ("invalid_date_h3.md",), + ("invalid_date_format.md",), + ("invalid_date_blank.md",), + ("invalid_date_broken_file.md",), + ], ) def test_parse_invalid_date(filename: str): - assert NygardParser.parse( + document = NygardParser.parse( Path(__file__).parent.joinpath(f"adr_docs/nygard/{filename}"), base_path=Path(__file__).parent, - ) == ADRDocument( - file_path=f"../adr_docs/nygard/{filename}", - title="1. Record architecture decisions", - date=None, - statuses=tuple(["Accepted"]), ) + assert document.date is None @pytest.mark.parametrize( ["filename"], - [("invalid_status_h3.md",)], + [ + ("invalid_status_h3.md",), + ("invalid_status_broken_file.md",), + ], ) -def test_parse_invalid_status(filename: str): - assert NygardParser.parse( +def test_parse_invalid_status(filename: str, adr_document_factory): + document = NygardParser.parse( Path(__file__).parent.joinpath(f"adr_docs/nygard/{filename}"), base_path=Path(__file__).parent, - ) == ADRDocument( - file_path=f"../adr_docs/nygard/{filename}", - title="1. Record architecture decisions", - date=datetime.fromisoformat("2024-01-20").date(), - statuses=tuple(["==INVALID_STATUS=="]), + ) + + assert ( + document.statuses + == adr_document_factory( + status=None, + statuses=tuple(), + ).statuses ) diff --git a/tests/test_plugin.py b/tests/test_plugin.py index e4dec71..e3e994a 100644 --- a/tests/test_plugin.py +++ b/tests/test_plugin.py @@ -28,7 +28,7 @@ def test_adr_summary(): ) as mock_listdir, patch( "os.path.isfile", return_value=True ): - summary = adr_summary(env=mkdocs_env, adr_path="docs/adr") + summary = adr_summary(env=mkdocs_env, adr_path="docs/adr", adr_style="nygard") mock_listdir.assert_called_once_with(PosixPath("/some/path/to/docs/adr")) fake_parser.parse.assert_called_once_with(