Merge "Added a state machine field"
This commit is contained in:
commit
39e36aed68
oslo_versionedobjects
@ -691,6 +691,75 @@ class EnumField(BaseEnumField):
|
||||
super(EnumField, self).__init__(**kwargs)
|
||||
|
||||
|
||||
class StateMachine(EnumField):
|
||||
"""A mixin that can be applied to an EnumField to enforce a state machine
|
||||
|
||||
e.g: Setting the code below on a field will ensure an object cannot
|
||||
transition from ERROR to ACTIVE
|
||||
|
||||
|
||||
class FakeStateMachineField(fields.EnumField, fields.StateMachine):
|
||||
|
||||
ACTIVE = 'ACTIVE'
|
||||
PENDING = 'PENDING'
|
||||
ERROR = 'ERROR'
|
||||
DELETED = 'DELETED'
|
||||
|
||||
ALLOWED_TRANSITIONS = {
|
||||
ACTIVE: {
|
||||
PENDING,
|
||||
ERROR,
|
||||
DELETED,
|
||||
},
|
||||
PENDING: {
|
||||
ACTIVE,
|
||||
ERROR
|
||||
},
|
||||
ERROR: {
|
||||
PENDING,
|
||||
},
|
||||
DELETED: {} # This is a terminal state
|
||||
}
|
||||
|
||||
_TYPES = (ACTIVE, PENDING, ERROR, DELETED)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FakeStateMachineField, self).__init__(self._TYPES, **kwargs)
|
||||
|
||||
"""
|
||||
# This is dict of states, that have dicts of states an object is
|
||||
# allowed to transition to
|
||||
|
||||
ALLOWED_TRANSITIONS = {}
|
||||
|
||||
def coerce(self, obj, attr, value):
|
||||
super(StateMachine, self).coerce(obj, attr, value)
|
||||
msg = _("%(object)s's are not allowed transition out of %(value)s "
|
||||
"state")
|
||||
|
||||
if attr in obj:
|
||||
current_value = getattr(obj, attr)
|
||||
else:
|
||||
return value
|
||||
|
||||
if current_value in self.ALLOWED_TRANSITIONS:
|
||||
|
||||
if value in self.ALLOWED_TRANSITIONS[current_value]:
|
||||
return value
|
||||
else:
|
||||
msg = _(
|
||||
"%(object)s's are not allowed to transition out of "
|
||||
"'%(current_value)s' state to '%(value)s' state, choose "
|
||||
"from %(options)r")
|
||||
msg = msg % {
|
||||
'object': obj.obj_name(),
|
||||
'current_value': current_value,
|
||||
'value': value,
|
||||
'options': [x for x in self.ALLOWED_TRANSITIONS[current_value]]
|
||||
}
|
||||
raise ValueError(msg)
|
||||
|
||||
|
||||
class UUIDField(AutoTypedField):
|
||||
AUTO_TYPE = UUID()
|
||||
|
||||
|
@ -18,6 +18,7 @@ import iso8601
|
||||
import mock
|
||||
import netaddr
|
||||
import six
|
||||
import testtools
|
||||
|
||||
from oslo_versionedobjects import _utils
|
||||
from oslo_versionedobjects import base as obj_base
|
||||
@ -65,6 +66,32 @@ class FakeEnumField(fields.BaseEnumField):
|
||||
AUTO_TYPE = FakeEnum()
|
||||
|
||||
|
||||
class FakeStateMachineField(fields.StateMachine):
|
||||
|
||||
ACTIVE = 'ACTIVE'
|
||||
PENDING = 'PENDING'
|
||||
ERROR = 'ERROR'
|
||||
|
||||
ALLOWED_TRANSITIONS = {
|
||||
ACTIVE: {
|
||||
PENDING,
|
||||
ERROR
|
||||
},
|
||||
PENDING: {
|
||||
ACTIVE,
|
||||
ERROR
|
||||
},
|
||||
ERROR: {
|
||||
PENDING,
|
||||
},
|
||||
}
|
||||
|
||||
_TYPES = (ACTIVE, PENDING, ERROR)
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
super(FakeStateMachineField, self).__init__(self._TYPES, **kwargs)
|
||||
|
||||
|
||||
class FakeEnumAltField(fields.BaseEnumField):
|
||||
AUTO_TYPE = FakeEnumAlt()
|
||||
|
||||
@ -225,6 +252,66 @@ class TestEnum(TestField):
|
||||
fields.EnumField, True)
|
||||
|
||||
|
||||
class TestStateMachine(TestField):
|
||||
|
||||
def test_good_transitions(self):
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class AnObject(obj_base.VersionedObject):
|
||||
fields = {
|
||||
'status': FakeStateMachineField(),
|
||||
}
|
||||
|
||||
obj = AnObject()
|
||||
|
||||
obj.status = FakeStateMachineField.ACTIVE
|
||||
obj.status = FakeStateMachineField.PENDING
|
||||
obj.status = FakeStateMachineField.ERROR
|
||||
obj.status = FakeStateMachineField.PENDING
|
||||
obj.status = FakeStateMachineField.ACTIVE
|
||||
|
||||
def test_bad_transitions(self):
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class AnObject(obj_base.VersionedObject):
|
||||
fields = {
|
||||
'status': FakeStateMachineField(),
|
||||
}
|
||||
|
||||
obj = AnObject()
|
||||
|
||||
with testtools.ExpectedException(
|
||||
ValueError,
|
||||
msg="AnObject's are not allowed transition out "
|
||||
"of 'ERROR' state to 'PENDING' state, choose from "
|
||||
"['PENDING']"):
|
||||
obj.status = FakeStateMachineField.ERROR
|
||||
obj.status = FakeStateMachineField.ACTIVE
|
||||
|
||||
def test_bad_initial_value(self):
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class AnObject(obj_base.VersionedObject):
|
||||
fields = {
|
||||
'status': FakeStateMachineField(),
|
||||
}
|
||||
|
||||
obj = AnObject()
|
||||
|
||||
with testtools.ExpectedException(ValueError):
|
||||
obj.status = "FOO"
|
||||
|
||||
def test_bad_updated_value(self):
|
||||
@obj_base.VersionedObjectRegistry.register
|
||||
class AnObject(obj_base.VersionedObject):
|
||||
fields = {
|
||||
'status': FakeStateMachineField(),
|
||||
}
|
||||
|
||||
obj = AnObject()
|
||||
|
||||
with testtools.ExpectedException(ValueError):
|
||||
obj.status = FakeStateMachineField.ACTIVE
|
||||
obj.status = "FOO"
|
||||
|
||||
|
||||
class TestInteger(TestField):
|
||||
def setUp(self):
|
||||
super(TestField, self).setUp()
|
||||
|
Loading…
x
Reference in New Issue
Block a user