Skip to content

Commit

Permalink
Merge pull request #14 from compi-migui/wip-template-validation
Browse files Browse the repository at this point in the history
Add template validation command
  • Loading branch information
lhh committed Jan 18, 2024
2 parents 22b5722 + 3add04e commit 8123e57
Show file tree
Hide file tree
Showing 11 changed files with 129 additions and 48 deletions.
File renamed without changes.
33 changes: 33 additions & 0 deletions jirate/jira_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,14 @@
import re
import sys
import yaml
from importlib.resources import files

import editor

from collections import OrderedDict
from jira.exceptions import JIRAError
from referencing import Registry, Resource
import jsonschema

from jirate.args import ComplicatedArgs, GenericArgs
from jirate.jboard import JiraProject, get_jira
Expand Down Expand Up @@ -537,6 +540,12 @@ def create_from_template(args):
with open(args.template_file, 'r') as yaml_file:
template = yaml.safe_load(yaml_file)

try:
validate_template(args)
except jsonschema.exceptions.ValidationError as e:
print(f"Provided template file is not valid: {args.template_file}")
raise e

for issue in template['issues']:
filed = {}
if 'description' not in issue:
Expand Down Expand Up @@ -567,6 +576,26 @@ def create_from_template(args):
return (0, True)


def validate_template(args):
with open(args.template_file, 'r') as yaml_file:
template = yaml.safe_load(yaml_file)

schema_dir = files('jirate').joinpath('schemas')
schemas = {}
for schema_file in ('template.yaml', 'issue.yaml', 'subtask.yaml'):
schemas[f"jirate:{schema_file}"] = yaml.safe_load(
schema_dir.joinpath(schema_file).read_text())

registry = Registry().with_contents([(k, v) for k, v in schemas.items()])
validator = jsonschema.Draft202012Validator(schema=schemas['jirate:template.yaml'], registry=registry)

# Will raise a ValidationError with details on what failed:
validator.validate(template)
# If we get here it means validation succeeded.
print(f"Template {args.template_file} is valid.")
return (0, True)


def new_subtask(args):
desc = None
parent_issue = args.project.issue(args.issue_id)
Expand Down Expand Up @@ -1126,6 +1155,10 @@ def create_parser():
cmd.add_argument('template_file', help='Path to the template file')
cmd.add_argument('-q', '--quiet', default=False, help='Only print new issue IDs after creation (for scripting)', action='store_true')

cmd = parser.command('validate', help='Validate a YAML template for use with the "template" command',
handler=validate_template)
cmd.add_argument('template_file', help='Path to the template file')

# TODO: build template from existing issue(s)

return parser
Expand Down
3 changes: 2 additions & 1 deletion schemas/issue.yaml → jirate/schemas/issue.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
$schema: "https://json-schema.org/draft/2020-12/schema"
$id: "jirate:issue.yaml"
type: object
description: JIRA issue.
required:
Expand All @@ -21,7 +22,7 @@ properties:
type: array
items:
oneOf:
- $ref: subtask.yaml
- $ref: "jirate:subtask.yaml"

# Fail validation if template sets values not defined in the schema
additionalProperties: False
1 change: 1 addition & 0 deletions schemas/subtask.yaml → jirate/schemas/subtask.yaml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
---
$schema: "https://json-schema.org/draft/2020-12/schema"
$id: "jirate:subtask.yaml"
type: object
description: JIRA sub-task. Similar to an issue, but can't set the issue_type or add sub-tasks to it.
required:
Expand Down
4 changes: 2 additions & 2 deletions schemas/template.yaml → jirate/schemas/template.yaml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
$schema: "https://json-schema.org/draft/2020-12/schema"
$id: "file://schemas/template.yaml"
$id: "jirate:template.yaml"
type: object
description: Template used to file Jira issues.
required:
Expand All @@ -12,7 +12,7 @@ properties:
type: array
items:
oneOf:
- $ref: issue.yaml
- $ref: "jirate:issue.yaml"

# Fail validation if template sets values not defined in the schema
additionalProperties: False
29 changes: 29 additions & 0 deletions jirate/tests/templates/bad-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
issues:
- issue_type: "Story"
summary: "Set up Jirate templates on my machine"
# Description terminated by two newlines
description: |
* {*}This is a multi-line description.{*}
** You can use any JIRA markup.
* {*}Make sure you keep indentation consistent.{*}
** This block is indented 2 spaces ahead of the "description" key.
** Any additional whitespace will be part of the issue description.
* {*}Termination{*}.
** You can terminate multi-line strings with two newlines.
subtasks:
- summary: "Clone the repo and install it"
description: "Description for subtask 1"
- summary: "Try creating an issue from template and verify it worked"
description: "Description for subtask 2"
- summary: "Eat a snack as a reward for all my hard work"
description: "Description for subtask 3"
- issue_type: "Story"
summary: "Make sure I stop using the default template"
deception: "<<<----- that's not how you spell 'description'!"
subtasks:
- summary: "Obtain or create custom templates"
description: "Description for subtask 1"
- summary: "Take a well-deserved nap as a reward for all my hard work"
description: "Description for subtask 2"
29 changes: 29 additions & 0 deletions jirate/tests/templates/good-template.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
issues:
- issue_type: "Story"
summary: "Set up Jirate templates on my machine"
# Description terminated by two newlines
description: |
* {*}This is a multi-line description.{*}
** You can use any JIRA markup.
* {*}Make sure you keep indentation consistent.{*}
** This block is indented 2 spaces ahead of the "description" key.
** Any additional whitespace will be part of the issue description.
* {*}Termination{*}.
** You can terminate multi-line strings with two newlines.
subtasks:
- summary: "Clone the repo and install it"
description: "Description for subtask 1"
- summary: "Try creating an issue from template and verify it worked"
description: "Description for subtask 2"
- summary: "Eat a snack as a reward for all my hard work"
description: "Description for subtask 3"
- issue_type: "Story"
summary: "Make sure I stop using the default template"
description: "People won't appreciate it if I keep filing the same issues over and over again."
subtasks:
- summary: "Obtain or create custom templates"
description: "Description for subtask 1"
- summary: "Take a well-deserved nap as a reward for all my hard work"
description: "Description for subtask 2"
49 changes: 6 additions & 43 deletions jirate/tests/test_schemas.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,55 +5,18 @@

import jsonschema
import pytest
from referencing import Registry, Resource
from referencing.exceptions import NoSuchResource

SCHEMA_DIR = Path('schemas').absolute()
SCHEMA_DIR = Path('jirate/schemas').absolute()
SCHEMAS = list(SCHEMA_DIR.rglob("*.yaml"))

TEMPLATE_DIR = Path('templates').absolute()
# NOTE: rglob does not follow symlinks prior to Python 3.13 See https://github.com/python/cpython/pull/102616
TEMPLATES = list(TEMPLATE_DIR.rglob("*.yaml"))


def _yaml_load(path):
return yaml.safe_load(path.read_text())


def retrieve_yaml(uri):
if uri.startswith("file://schemas"):
path = SCHEMA_DIR / Path(uri.removeprefix("file://schemas/"))
elif uri.startswith("file://templates"):
path = TEMPLATE_DIR / Path(uri.removeprefix("file://templates/"))
else:
raise NoSuchResource(ref=uri)

contents = yaml.safe_load(path.read_text())
return Resource.from_contents(contents)


@pytest.fixture(scope="session")
def template_validator():
"""Schema validator for templates.
This is a performance optimization in which the validator
is instantiated only once per session.
"""
schema = _yaml_load(SCHEMA_DIR / Path('template.yaml'))
registry = Registry(retrieve=retrieve_yaml)

return jsonschema.Draft202012Validator(
schema=schema,
registry=registry,
)
def test_at_least_one_schema():
# Detects issues in the provided schemas path, as pytest quietly skips
# parametrized tests with empty parameters
assert len(SCHEMAS) > 0


@pytest.mark.parametrize("schema", SCHEMAS)
def test_schema(schema):
schema = _yaml_load(schema)
schema = yaml.safe_load(schema.read_text())
jsonschema.Draft202012Validator.check_schema(schema)


@pytest.mark.parametrize("template", TEMPLATES)
def test_template(template, template_validator):
template_validator.validate(_yaml_load(template))
25 changes: 25 additions & 0 deletions jirate/tests/test_templates.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env python3

from pathlib import Path
from collections import namedtuple

import pytest
from jsonschema.exceptions import ValidationError

from jirate.jira_cli import validate_template

TEMPLATE_DIR = Path('jirate/tests/templates').absolute()
FakeArgs = namedtuple('FakeArgs', 'template_file')


def test_validate_templates_good():
# Validating a known-good template should succeed
good_args = FakeArgs(template_file=f"{TEMPLATE_DIR / 'good-template.yaml'}")
validate_template(good_args)


def test_validate_templates_bad():
# Validating a known-bad template should fail
bad_args = FakeArgs(template_file=f"{TEMPLATE_DIR / 'bad-template.yaml'}")
with pytest.raises(ValidationError):
validate_template(bad_args)
2 changes: 2 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,5 @@ jira>=3.6.0
python-dateutil
toolchest
prettytable
jsonschema>=4.18.0a1
referencing
2 changes: 0 additions & 2 deletions test-requirements.txt
Original file line number Diff line number Diff line change
@@ -1,3 +1 @@
jsonschema>=4.18.0a1
pytest
referencing

0 comments on commit 8123e57

Please sign in to comment.