Skip to content

Commit

Permalink
Create example XYZ validation plugin
Browse files Browse the repository at this point in the history
  • Loading branch information
austinmatherne-wk committed Sep 18, 2023
1 parent 09e110e commit 5eb95af
Show file tree
Hide file tree
Showing 10 changed files with 342 additions and 0 deletions.
44 changes: 44 additions & 0 deletions arelle/examples/plugin/validate/XYZ/ValidationPluginExtension.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
"""
See COPYRIGHT.md for copyright information.
"""
from __future__ import annotations

from typing import Any

from arelle.ModelDocument import LoadingException, ModelDocument
from arelle.ModelXbrl import ModelXbrl
from arelle.typing import TypeGetText
from arelle.utils.validate.ValidationPlugin import ValidationPlugin

_: TypeGetText


class ValidationPluginExtension(ValidationPlugin):
def modelDocumentPullLoader(
self,
modelXbrl: ModelXbrl,
normalizedUri: str,
filepath: str,
isEntry: bool,
namespace: str | None,
*args: Any,
**kwargs: Any,
) -> ModelDocument | LoadingException | None:
if self.disclosureSystemFromPluginSelected(modelXbrl):
return LoadingException(_("XYZ validation plugin is a template for new validation plugins and shouldn't be used directly."))
return None

def modelXbrlLoadComplete(
self,
modelXbrl: ModelXbrl,
*args: Any,
**kwargs: Any,
) -> None:
if self.disclosureSystemFromPluginSelected(modelXbrl):
if modelXbrl.modelDocument is None:
modelXbrl.error(
codes="XYZ.01.01",
msg=_("An XBRL Report Package is required but could not be loaded"),
modelObject=modelXbrl,
)
return None
72 changes: 72 additions & 0 deletions arelle/examples/plugin/validate/XYZ/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
"""
See COPYRIGHT.md for copyright information.
Filer Manual Guidelines: https://www.example.com/fake-xyz-filer-manual-v0.0.1.pdf
"""

from __future__ import annotations

from pathlib import Path
from typing import Any

from arelle.ModelDocument import LoadingException, ModelDocument
from arelle.Version import authorLabel, copyrightLabel
from . import rules
from .ValidationPluginExtension import ValidationPluginExtension

DISCLOSURE_SYSTEM_VALIDATION_TYPE = "XYZ"

DISCLOSURE_SYSTEM_2022 = "XYZ 2022"
DISCLOSURE_SYSTEM_2023 = "XYZ 2023"


validationPlugin = ValidationPluginExtension(
disclosureSystemConfigUrl=Path(__file__).parent / "resources" / "config.xml",
validationTypes=[DISCLOSURE_SYSTEM_VALIDATION_TYPE],
validationRulesModule=rules,
)


def disclosureSystemTypes(*args: Any, **kwargs: Any) -> tuple[tuple[str, str], ...]:
return validationPlugin.disclosureSystemTypes


def disclosureSystemConfigURL(*args: Any, **kwargs: Any) -> str:
return validationPlugin.disclosureSystemConfigURL


def modelDocumentPullLoader(*args: Any, **kwargs: Any) -> ModelDocument | LoadingException | None:
return validationPlugin.modelDocumentPullLoader(*args, **kwargs)


def modelXbrlLoadComplete(*args: Any, **kwargs: Any) -> None:
return validationPlugin.modelXbrlLoadComplete(*args, **kwargs)


def validateXbrlFinally(*args: Any, **kwargs: Any) -> None:
return validationPlugin.validateXbrlFinally(*args, **kwargs)


def validateXbrlDtsDocument(*args: Any, **kwargs: Any) -> None:
return validationPlugin.validateXbrlDtsDocument(*args, **kwargs)


def validateFinally(*args: Any, **kwargs: Any) -> None:
return validationPlugin.validateFinally(*args, **kwargs)


__pluginInfo__ = {
"name": "Validate XYZ",
"version": "0.0.1",
"description": "Example validation plugin for the fictitious XYZ taxonomy.",
"license": "Apache-2",
"author": authorLabel,
"copyright": copyrightLabel,
"DisclosureSystem.Types": disclosureSystemTypes,
"DisclosureSystem.ConfigURL": disclosureSystemConfigURL,
"ModelDocument.PullLoader": modelDocumentPullLoader,
"ModelXbrl.LoadComplete": modelXbrlLoadComplete,
"Validate.XBRL.Finally": validateXbrlFinally,
"Validate.XBRL.DTS.document": validateXbrlDtsDocument,
"Validate.Finally": validateFinally,
}
16 changes: 16 additions & 0 deletions arelle/examples/plugin/validate/XYZ/resources/config.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="UTF-8"?>
<DisclosureSystems
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../../../config/disclosuresystems.xsd">
<!-- see arelle/config/disclosuresystems.xml for full comments -->
<DisclosureSystem
names="XYZ 2023|XYZ-2023|xyz-2023|XYZ|xyz"
description="Checks for example XYZ validation plugin for year 2023"
validationType="XYZ"
/>
<DisclosureSystem
names="XYZ 2022|XYZ-2022|xyz-2022"
description="Checks for example XYZ validation plugin for year 2022"
validationType="XYZ"
/>
</DisclosureSystems>
Empty file.
111 changes: 111 additions & 0 deletions arelle/examples/plugin/validate/XYZ/rules/rules01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
"""
See COPYRIGHT.md for copyright information.
"""
from __future__ import annotations

from typing import Any, Iterable, cast

from arelle.ValidateXbrl import ValidateXbrl
from arelle.XmlValidate import VALID
from arelle.typing import TypeGetText
from arelle.utils.PluginHooks import ValidationHook
from arelle.utils.validate.Decorator import validation
from arelle.utils.validate.Validation import Validation
from .. import DISCLOSURE_SYSTEM_2022, DISCLOSURE_SYSTEM_2023

_: TypeGetText

POSITIVE_FACTS_PLUGIN_CACHE_KEY = "positiveFacts"


# rule 01.01 (2022)
@validation(
hook=ValidationHook.XBRL_FINALLY,
disclosureSystems=DISCLOSURE_SYSTEM_2022,
)
def rule01_01_2022(
pluginCache: dict[str, Any],
val: ValidateXbrl,
*args: Any,
**kwargs: Any,
) -> Iterable[Validation] | None:
if "Cash" not in val.modelXbrl.factsByLocalName:
yield Validation.error(
codes="XYZ.01.01",
msg=_("Cash must be reported."),
modelObject=val.modelXbrl,
)


# rule 01.01 (2023)
@validation(
hook=ValidationHook.XBRL_FINALLY,
disclosureSystems=DISCLOSURE_SYSTEM_2023,
)
def rule01_01_2023(
pluginCache: dict[str, Any],
val: ValidateXbrl,
*args: Any,
**kwargs: Any,
) -> Iterable[Validation] | None:
conceptLocalNamesWithPositiveFactValues = pretendExpensiveOperation(pluginCache, val)
if "Cash" not in conceptLocalNamesWithPositiveFactValues:
yield Validation.warning(
codes="XYZ.01.01",
msg=_("Cash should be reported."),
modelObject=val.modelXbrl,
)


@validation(hook=ValidationHook.FINALLY)
def rule01_02(
pluginCache: dict[str, Any],
val: ValidateXbrl,
*args: Any,
**kwargs: Any,
) -> Iterable[Validation] | None:
numXbrlErrors = len(val.modelXbrl.errors)
if numXbrlErrors > 0:
yield Validation.error(
codes="XYZ.01.02",
msg=_("Invalid report %(numXbrlErrors)s detected."),
modelObject=val.modelXbrl,
numXbrlErrors=numXbrlErrors,
)


@validation(
hook=ValidationHook.XBRL_FINALLY,
disclosureSystems=DISCLOSURE_SYSTEM_2023,
)
def rule01_03(
pluginCache: dict[str, Any],
val: ValidateXbrl,
*args: Any,
**kwargs: Any,
) -> Iterable[Validation] | None:
conceptLocalNamesWithPositiveFactValues = pretendExpensiveOperation(pluginCache, val)
if "UnitsSold" not in conceptLocalNamesWithPositiveFactValues:
yield Validation.error(
codes="XYZ.01.03",
msg=_("UnitsSold must be reported."),
modelObject=val.modelXbrl,
)


def pretendExpensiveOperation(
pluginCache: dict[str, Any], val: ValidateXbrl
) -> set[str]:
positiveFactConcepts: set[str] | None = pluginCache.get(
POSITIVE_FACTS_PLUGIN_CACHE_KEY
)
if positiveFactConcepts is None:
positiveFactConcepts = {
fact.localName
for fact in val.modelXbrl.facts
if fact.isNumeric
and getattr(fact, "xValid", 0) >= VALID
and cast(int, fact.xValue) > 0
}
pluginCache[POSITIVE_FACTS_PLUGIN_CACHE_KEY] = positiveFactConcepts
return positiveFactConcepts
57 changes: 57 additions & 0 deletions arelle/examples/plugin/validate/XYZ/rules/rules02.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
"""
See COPYRIGHT.md for copyright information.
"""
from __future__ import annotations

from typing import Any, Iterable

from arelle import XbrlConst
from arelle.ModelDocument import ModelDocument, Type as ModelDocumentType
from arelle.ValidateXbrl import ValidateXbrl
from arelle.typing import TypeGetText
from arelle.utils.PluginHooks import ValidationHook
from arelle.utils.validate.Decorator import validation
from arelle.utils.validate.Validation import Validation
from .. import DISCLOSURE_SYSTEM_2022

_: TypeGetText


@validation(hook=ValidationHook.XBRL_DTS_DOCUMENT)
def rule02_01(
pluginCache: dict[str, Any],
val: ValidateXbrl,
modelDocument: ModelDocument,
isFilingDocument: bool,
*args: Any,
**kwargs: Any,
) -> Iterable[Validation] | None:
if (
modelDocument.type == ModelDocumentType.SCHEMA
and modelDocument.targetNamespace is not None
and len(modelDocument.targetNamespace) > 100
):
yield Validation.error(
codes="XYZ.02.01",
msg=_("TargetNamespace is too long %(namespace)s."),
modelObject=val.modelXbrl,
namespace=modelDocument.targetNamespace,
)


@validation(
hook=ValidationHook.XBRL_FINALLY,
excludeDisclosureSystems=DISCLOSURE_SYSTEM_2022,
)
def rule02_02(
pluginCache: dict[str, Any],
val: ValidateXbrl,
*args: Any,
**kwargs: Any,
) -> Iterable[Validation] | None:
if val.modelXbrl.relationshipSet(XbrlConst.summationItem):
yield Validation.error(
codes="XYZ.02.02",
msg=_("XBRL 2.1 calculations detected. XYZ 2023 taxonomy requires calc 1.1."),
modelObject=val.modelXbrl,
)
4 changes: 4 additions & 0 deletions distro.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,10 @@
(os.path.normcase("arelle/locale"), "locale"),
(os.path.normcase("arelle/examples"), "examples"),
(os.path.normcase("arelle/examples/plugin"), os.path.normcase("examples/plugin")),
(os.path.normcase("arelle/examples/plugin/validate"), os.path.normcase("examples/plugin/validate")),
(os.path.normcase("arelle/examples/plugin/validate/XYZ"), os.path.normcase("examples/plugin/validate/XYZ")),
(os.path.normcase("arelle/examples/plugin/validate/XYZ/resources"), os.path.normcase("examples/plugin/validate/XYZ/resources")),
(os.path.normcase("arelle/examples/plugin/validate/XYZ/rules"), os.path.normcase("examples/plugin/validate/XYZ/rules")),
(
os.path.normcase("arelle/examples/plugin/locale/fr/LC_MESSAGES"),
os.path.normcase("examples/plugin/locale/fr/LC_MESSAGES"),
Expand Down
1 change: 1 addition & 0 deletions docs/source/plugins/development/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,5 +6,6 @@
:::{toctree}
getting_started
publishing
validation
hooks
:::
36 changes: 36 additions & 0 deletions docs/source/plugins/development/validation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# Create a Validation Plugin

:::{index} Create a Validation Plugin
:::

One of the more common reasons to build a plugin is to add support for taxonomy or jurisdiction specific validation rules.
To help accelerate the process of creating new validation plugins there's a [ValidationPlugin][validation-plugin] class
and [@validation][validation-decorator] decorator for writing validation rule functions along with an [example template validation plugin][example-plugin]
that demonstrates how to use them together and can be copied as a starting point.

## Steps to Create a New Validation Plugin

1. Copy the [XYZ validation plugin][example-plugin] from the examples directory into the [Arelle/plugins/validate directory][validations-directory].
2. Rename the plugin module from `XYZ` to the name of the taxonomy or jurisdiction you're implementing validation rules for.
3. Update the resources/config.xml disclosure system file with details for your plugin.
4. Update the `__init__.py` module:
1. If there's a filer manual or other documentation for the rules you're implementing available online,
update the comment at the top of the `__init__.py` module with a link.
2. Update the `__pluginInfo__` details with the name and description of your plugin.
3. Update the `DISCLOSURE_SYSTEM_VALIDATION_TYPE` variable to match the validation type you used in resources/config.xml.
4. Remove any of the plugin hooks you don't need, including the functions defined in the `ValidationPluginExtension` class.
5. Implement the plugin specific validation rules in the rules directory using functions and the [@validation][validation-decorator] decorator.
6. [Open a PR][contributing-code] to have your plugin merged into Arelle.

## Example of a validation rule

:::{literalinclude} ../../../../arelle/examples/plugin/validate/XYZ/rules/rules01.py
:start-after: "# rule 01.01 (2022)"
:end-before: "# rule 01.01 (2023)"
:::

[validation-plugin]: #arelle.utils.validate.ValidationPlugin.ValidationPlugin
[validation-decorator]: #arelle.utils.validate.Decorator.validation
[example-plugin]: https://github.com/Arelle/Arelle/tree/master/arelle/examples/plugin/validate/XYZ
[validations-directory]: https://github.com/Arelle/Arelle/tree/master/arelle/plugin/validate
[contributing-code]: project:../../contributing.md#contributing-code
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ module = [
'arelle.XmlValidate',
'arelle.XmlValidateConst',
'arelle.XmlValidateSchema',
'arelle.examples.plugin.validate.*',
'arelle.formula.FactAspectsCache',
'arelle.formula.XPathContext',
'arelle.formula.XPathParser',
Expand Down

0 comments on commit 5eb95af

Please sign in to comment.