Skip to content

Commit

Permalink
Add YAML validation using pykwalify
Browse files Browse the repository at this point in the history
  • Loading branch information
AlexandreDecan committed Dec 25, 2015
1 parent 3bf3b16 commit cecdaa9
Show file tree
Hide file tree
Showing 9 changed files with 171 additions and 61 deletions.
35 changes: 26 additions & 9 deletions docs/format.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ Statecharts can be defined using a YAML format.

A YAML definition of a statechart can be easily imported to a :py:class:`~sismic.model.StateChart` instance.
The module :py:mod:`sismic.io` provides a convenient loader :py:func:`~sismic.io.import_from_yaml`
which takes a textual YAML definition of a statechart. It also provides ways to export statechart to YAML.
which takes a textual YAML definition of a statechart. It also provides a way to export statechart to YAML.

.. automodule:: sismic.io
:members: import_from_yaml, export_to_yaml
Expand All @@ -38,11 +38,10 @@ For example:
assert isinstance(statechart, model.StateChart)
Although the parser identifies most syntax problems, a :py:class:`~sismic.model.StateChart` instance has a
:py:meth:`~sismic.model.StateChart.validate` method that can perform numerous other checks.
This method either returns ``True`` if the statechart *seems* to
be valid, or raises an ``AssertionError`` exception with a meaningful message.

The parser performs an automatic validation against a YAML schema, and also performs additional
validation using :py:meth:`~sismic.model.StateChart.validate` method
of the resulting :py:class:`~sismic.model.StateChart` instance
(see :ref:`yaml_schema`).

Statechart elements
*******************
Expand All @@ -61,7 +60,7 @@ The root of the YAML file **must** declare a statechart:
statechart:
name: Name of this statechart
initial: name of the initial state
description: Description of this statechart
The ``name`` and the ``initial`` state are mandatory.
You can declare code to execute on the initialization of the statechart using ``on entry``, as follows:
Expand Down Expand Up @@ -235,6 +234,25 @@ Sismic, but can be used to provide additional information about the statechart.
.. literalinclude:: ../examples/microwave.yaml
:language: yaml



.. _yaml_schema:


YAML statechart validation
**************************

Function :py:func:`~sismic.io.import_from_yaml` performs an automatic validation against the following YAML
schema file (see `pykwalify <https://github.com/Grokzen/pykwalify/>`__ for more information about the format).

.. literalinclude:: ../sismic/schema.yaml
:language: yaml

This function also validates the resulting statechart using :py:meth:`~sismic.model.StateChart.validate`:

.. automethod:: sismic.model.StateChart.validate


Defining statecharts in Python
------------------------------

Expand All @@ -249,5 +267,4 @@ states, transitions and events. Apart from the mixin classes, it defines:
:members: Event, Transition, BasicState, CompoundState, OrthogonalState, HistoryState, FinalState, StateChart
:member-order: bysource

Consider the source of :py:mod:`sismic.io` as an example of how to construct a statechart using :py:mod:`sismic.model`.

Consider the source of :py:mod:`sismic.io` as an example of how to construct a statechart using :py:mod:`sismic.model`.
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pyyaml
pykwalify
coverage
wheel
sphinx
2 changes: 1 addition & 1 deletion setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
# your project is installed. For an analysis of "install_requires" vs pip's
# requirements files see:
# https://packaging.python.org/en/latest/requirements.html
install_requires=['pyyaml'],
install_requires=['pyyaml>=3.11', 'pykwalify>=1.5.0'],

# List additional groups of dependencies here (e.g. development
# dependencies). You can install these using the following syntax,
Expand Down
2 changes: 1 addition & 1 deletion sismic/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from . import evaluator, io, model, interpreter, stories

__description__ = 'Sismic Interactive State Machine Interpreter and Checker'
__version__ = '0.11.4'
__version__ = '0.11.5'
__url__ = 'https://github.com/AlexandreDecan/sismic/'
__author__ = 'Alexandre Decan'
__email__ = 'alexandre.decan@lexpage.net'
Expand Down
26 changes: 22 additions & 4 deletions sismic/io.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,35 @@
import yaml
import os
from pykwalify.core import Core
from collections import OrderedDict
from .model import Event, Transition, StateChart, BasicState, CompoundState, OrthogonalState, HistoryState, FinalState
from .model import StateMixin, ActionStateMixin, TransitionStateMixin, CompositeStateMixin


def import_from_yaml(data: str) -> StateChart:
SCHEMA_PATH = os.path.join(os.path.dirname(__file__), 'schema.yaml')


def import_from_yaml(statechart: str, validate_yaml=True, validate_statechart=True) -> StateChart:
"""
Import a statechart from a YAML representation.
YAML is first validated against ``io.SCHEMA``.
:param data: string or any equivalent object
:return: a StateChart instance
:param statechart: string or any equivalent object
:param validate_yaml: set to ``False`` to disable yaml validation.
:param validate_statechart: set to ``False`` to disable statechart validation
(see ``model.StateChart.validate``).
:return: a ``StateChart`` instance
"""
return _import_from_dict(yaml.load(data)['statechart'])
statechart_data = yaml.load(statechart)
if validate_yaml:
checker = Core(source_data=statechart_data, schema_files=[SCHEMA_PATH])
checker.validate(raise_exception=True)

sc = _import_from_dict(statechart_data['statechart'])
if validate_statechart:
sc.validate()

return sc


def _import_from_dict(data: dict) -> StateChart:
Expand Down
83 changes: 83 additions & 0 deletions sismic/schema.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
schema;contract:
type: seq
sequence:
- type: map
mapping:
"before":
type: str
- type: map
mapping:
"after":
type: str
- type: map
mapping:
"always":
type: str

schema;transition:
type: map
mapping:
"target":
type: str
"event":
type: str
"guard":
type: str
"action":
type: str
"contract":
include: contract

schema;state:
type: map
mapping:
"name":
type: str
required: yes
"initial":
type: str
"on entry":
type: str
"on exit":
type: str
"type":
type: str
enum: [final, shallow history, deep history]
"states":
type: seq
sequence:
- include: state
"parallel states":
type: seq
sequence:
- include: state
"transitions":
type: seq
sequence:
- include: transition
"contract":
include: contract

type: map
mapping:
"statechart":
type: map
required: yes
mapping:
"name":
type: str
required: yes
"description":
type: str
"on entry":
type: str
"initial":
type: str
required: yes
"states":
type: seq
required: yes
sequence:
- include: state
"contract":
include: contract
80 changes: 36 additions & 44 deletions tests/test_io.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,54 +3,46 @@


class ImportFromYamlTests(unittest.TestCase):
def test_simple(self):
content = open('tests/yaml/simple.yaml')
sc =io.import_from_yaml(content)
self.assertTrue(sc.validate())

def test_composite(self):
content = open('tests/yaml/composite.yaml')
sc = io.import_from_yaml(content)
self.assertTrue(sc.validate())

def test_history(self):
content = open('tests/yaml/history.yaml')
sc = io.import_from_yaml(content)
self.assertTrue(sc.validate())

def test_actions(self):
content = open('tests/yaml/actions.yaml')
sc = io.import_from_yaml(content)
self.assertTrue(sc.validate())

def test_elevator(self):
content = open('examples/elevator.yaml')
sc = io.import_from_yaml(content)
self.assertTrue(sc.validate())

def test_contract(self):
content = open('examples/elevator_contract.yaml')
sc = io.import_from_yaml(content)
self.assertTrue(sc.validate())
def test_yaml_tests(self):
io.import_from_yaml(open('tests/yaml/actions.yaml'))
io.import_from_yaml(open('tests/yaml/composite.yaml'))
io.import_from_yaml(open('tests/yaml/deep_history.yaml'))
io.import_from_yaml(open('tests/yaml/history.yaml'))
io.import_from_yaml(open('tests/yaml/infinite.yaml'))
io.import_from_yaml(open('tests/yaml/internal.yaml'))
io.import_from_yaml(open('tests/yaml/nested_parallel.yaml'))
io.import_from_yaml(open('tests/yaml/nondeterministic.yaml'))
io.import_from_yaml(open('tests/yaml/parallel.yaml'))
io.import_from_yaml(open('tests/yaml/simple.yaml'))
io.import_from_yaml(open('tests/yaml/timer.yaml'))

def test_examples(self):
io.import_from_yaml(open('examples/elevator.yaml'))
io.import_from_yaml(open('examples/elevator_contract.yaml'))
io.import_from_yaml(open('examples/microwave.yaml'))
io.import_from_yaml(open('examples/tester_elevator_7th_floor_never_reached.yaml'))
io.import_from_yaml(open('examples/tester_elevator_moves_after_10s.yaml'))
io.import_from_yaml(open('examples/writer_options.yaml'))


class ExportToDictYAMLTests(unittest.TestCase):
def test_simple(self):
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/simple.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/simple.yaml')))

def test_composite(self):
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/composite.yaml')))
def test_yaml_tests(self):
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/actions.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/composite.yaml')))

def test_history(self):
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/deep_history.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/history.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/history.yaml')))

def test_actions(self):
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/actions.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/actions.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/infinite.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/internal.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/nested_parallel.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/nondeterministic.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/parallel.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/simple.yaml')))
io.export_to_yaml(io.import_from_yaml(open('tests/yaml/timer.yaml')))

def test_contract(self):
io.export_to_yaml(io.import_from_yaml(open('examples/elevator.yaml')))
def test_examples(self):
io.export_to_yaml(io.import_from_yaml(open('examples/elevator.yaml')))
io.export_to_yaml(io.import_from_yaml(open('examples/elevator_contract.yaml')))
io.export_to_yaml(io.import_from_yaml(open('examples/microwave.yaml')))
io.export_to_yaml(io.import_from_yaml(open('examples/tester_elevator_7th_floor_never_reached.yaml')))
io.export_to_yaml(io.import_from_yaml(open('examples/tester_elevator_moves_after_10s.yaml')))
io.export_to_yaml(io.import_from_yaml(open('examples/writer_options.yaml')))
2 changes: 1 addition & 1 deletion tests/yaml/actions.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ statechart:
action: |
x += 1
- name: s2
on_entry: |
on entry: |
y += 1
transitions:
- target: s3
Expand Down
1 change: 0 additions & 1 deletion tests/yaml/deep_history.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ statechart:
states:
- name: active.H*
type: deep history
deep: True
- name: concurrent_processes
parallel states:
- name: process_1
Expand Down

0 comments on commit cecdaa9

Please sign in to comment.