From 3bba8c69c39cd37d5f813f243ed6ca62ac408c2b Mon Sep 17 00:00:00 2001 From: Chris Block Date: Fri, 17 Oct 2014 19:54:51 -0500 Subject: [PATCH 1/7] Implement reflection based state and event registration --- rakefile.rb | 20 + src/Automatonymous.Tests/Activity_Specs.cs | 17 - src/Automatonymous.Tests/Anytime_Specs.cs | 6 - .../AsyncActivity_Specs.cs | 4 - src/Automatonymous.Tests/Combine_Specs.cs | 4 - .../DataActivity_Specs.cs | 5 - src/Automatonymous.Tests/Declarative_Specs.cs | 8 - src/Automatonymous.Tests/Dependency_Specs.cs | 4 - src/Automatonymous.Tests/EventLift_Specs.cs | 8 - .../EventObservable_Specs.cs | 4 - src/Automatonymous.Tests/Event_Specs.cs | 3 - src/Automatonymous.Tests/Exception_Specs.cs | 8 - .../FilterExpression_Specs.cs | 5 - src/Automatonymous.Tests/Group_Specs.cs | 8 - .../InstanceLift_Specs.cs | 8 - .../Introspection_Specs.cs | 9 - src/Automatonymous.Tests/Observable_Specs.cs | 5 - .../SerializeState_Specs.cs | 5 - src/Automatonymous.Tests/State_Specs.cs | 5 - src/Automatonymous.Tests/Transition_Specs.cs | 5 - src/Automatonymous.Tests/Visualizer_Specs.cs | 12 +- .../AutomatonymousStateMachine.cs | 803 ++++++++++-------- src/Automatonymous/Event.cs | 2 +- src/Automatonymous/State.cs | 2 +- .../CompositeEvent_Specs.cs | 4 - .../Fault_Specs.cs | 9 - .../PublishRequest_Specs.cs | 3 - .../Publish_Specs.cs | 3 - .../RemoveWhen_Specs.cs | 8 +- .../Respond_Specs.cs | 3 - .../SimpleStateMachine_Specs.cs | 5 - .../Testing_Specs.cs | 5 - .../UncorrelatedMessage_Specs.cs | 4 - .../Vanilla_Specs.cs | 5 - 34 files changed, 478 insertions(+), 531 deletions(-) diff --git a/rakefile.rb b/rakefile.rb index 0ea8e82..c5381d3 100644 --- a/rakefile.rb +++ b/rakefile.rb @@ -45,6 +45,26 @@ asm.namespaces "System", "System.Reflection", "System.Runtime.InteropServices" end +#desc "Update the common version information for the build. You can call this task without building." +#asmver :global_version do |asm| +# # Assembly file config +# asm.file_path = 'src/SolutionVersion.cs' +# +# #asm.namespaces "System", "System.Reflection", "System.Runtime.InteropServices" +# +# asm.attributes assembly_description: "Automatonymous is an open source state machine library.", +# assembly_version: FORMAL_VERSION, +# assembly_file_version: FORMAL_VERSION, +# assembly_informational_version: "#{BUILD_VERSION}", +# assembly_copyright: COPYRIGHT, +# assembly_product: PRODUCT, +# assembly_title: PRODUCT, +# com_visible: false, +# CLS_compliant: true +# +# #asm.out = StringIO.new +#end + desc "Prepares the working directory for a new build" task :clean do FileUtils.rm_rf props[:output] diff --git a/src/Automatonymous.Tests/Activity_Specs.cs b/src/Automatonymous.Tests/Activity_Specs.cs index 80d5834..3834e86 100644 --- a/src/Automatonymous.Tests/Activity_Specs.cs +++ b/src/Automatonymous.Tests/Activity_Specs.cs @@ -50,10 +50,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Running)); @@ -101,10 +97,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Initialized); - Initially( When(Initialized) .TransitionTo(Running)); @@ -159,10 +151,6 @@ class InstanceStateMachine : public InstanceStateMachine() { - State(() => Running); - - Event(() => Initialized); - Initially( When(Initialized) .Finalize()); @@ -233,11 +221,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Initializing); - State(() => Running); - - Event(() => Initialized); - During(Initializing, When(Initialized) .TransitionTo(Running)); diff --git a/src/Automatonymous.Tests/Anytime_Specs.cs b/src/Automatonymous.Tests/Anytime_Specs.cs index 17ac414..06937c6 100644 --- a/src/Automatonymous.Tests/Anytime_Specs.cs +++ b/src/Automatonymous.Tests/Anytime_Specs.cs @@ -84,12 +84,6 @@ class TestStateMachine : { public TestStateMachine() { - State(() => Ready); - - Event(() => Init); - Event(() => Hello); - Event(() => EventA); - Initially( When(Init) .TransitionTo(Ready)); diff --git a/src/Automatonymous.Tests/AsyncActivity_Specs.cs b/src/Automatonymous.Tests/AsyncActivity_Specs.cs index efbdc65..2f56605 100644 --- a/src/Automatonymous.Tests/AsyncActivity_Specs.cs +++ b/src/Automatonymous.Tests/AsyncActivity_Specs.cs @@ -67,10 +67,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Create); - During(Initial, When(Create) .Then(() => new SetValueAsyncActivity()) diff --git a/src/Automatonymous.Tests/Combine_Specs.cs b/src/Automatonymous.Tests/Combine_Specs.cs index 9543aa8..b70860b 100644 --- a/src/Automatonymous.Tests/Combine_Specs.cs +++ b/src/Automatonymous.Tests/Combine_Specs.cs @@ -72,11 +72,7 @@ class TestStateMachine : public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Waiting); - Event(() => Start); - Event(() => First); - Event(() => Second); Event(() => Third, x => x.CompositeStatus, First, Second); Initially( diff --git a/src/Automatonymous.Tests/DataActivity_Specs.cs b/src/Automatonymous.Tests/DataActivity_Specs.cs index 3d43c52..51906fe 100644 --- a/src/Automatonymous.Tests/DataActivity_Specs.cs +++ b/src/Automatonymous.Tests/DataActivity_Specs.cs @@ -75,11 +75,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Initialized); - Event(() => PassedValue); - During(Initial, When(Initialized) .Then((instance, data) => instance.Value = data.Value) diff --git a/src/Automatonymous.Tests/Declarative_Specs.cs b/src/Automatonymous.Tests/Declarative_Specs.cs index bf5ed42..1775f71 100644 --- a/src/Automatonymous.Tests/Declarative_Specs.cs +++ b/src/Automatonymous.Tests/Declarative_Specs.cs @@ -68,10 +68,6 @@ public TopInstanceStateMachine() { InstanceState(x => x.Top); - State(() => Greeted); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Greeted)); @@ -89,10 +85,6 @@ public BottomInstanceStateMachine() { InstanceState(x => x.Bottom); - State(() => Ignored); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Ignored)); diff --git a/src/Automatonymous.Tests/Dependency_Specs.cs b/src/Automatonymous.Tests/Dependency_Specs.cs index 970857d..4497d87 100644 --- a/src/Automatonymous.Tests/Dependency_Specs.cs +++ b/src/Automatonymous.Tests/Dependency_Specs.cs @@ -102,10 +102,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Create); - During(Initial, When(Create) .Then(() => new CalculateValueActivity(new LocalCalculator())) diff --git a/src/Automatonymous.Tests/EventLift_Specs.cs b/src/Automatonymous.Tests/EventLift_Specs.cs index a2185c3..33eb84b 100644 --- a/src/Automatonymous.Tests/EventLift_Specs.cs +++ b/src/Automatonymous.Tests/EventLift_Specs.cs @@ -50,10 +50,6 @@ class InstanceStateMachine : { public InstanceStateMachine() { - State(() => Running); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Running)); @@ -107,10 +103,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Running)); diff --git a/src/Automatonymous.Tests/EventObservable_Specs.cs b/src/Automatonymous.Tests/EventObservable_Specs.cs index 8913e62..4401263 100644 --- a/src/Automatonymous.Tests/EventObservable_Specs.cs +++ b/src/Automatonymous.Tests/EventObservable_Specs.cs @@ -86,10 +86,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Running)); diff --git a/src/Automatonymous.Tests/Event_Specs.cs b/src/Automatonymous.Tests/Event_Specs.cs index 44c8491..b4c4b5a 100644 --- a/src/Automatonymous.Tests/Event_Specs.cs +++ b/src/Automatonymous.Tests/Event_Specs.cs @@ -73,9 +73,6 @@ class TestStateMachine : { public TestStateMachine() { - Event(() => Hello); - Event(() => EventA); - Event(() => EventInt); } public Event Hello { get; private set; } diff --git a/src/Automatonymous.Tests/Exception_Specs.cs b/src/Automatonymous.Tests/Exception_Specs.cs index 98aa374..2dd8041 100644 --- a/src/Automatonymous.Tests/Exception_Specs.cs +++ b/src/Automatonymous.Tests/Exception_Specs.cs @@ -84,10 +84,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Failed); - - Event(() => Initialized); - During(Initial, When(Initialized) .Try(x => x.Then(instance => instance.Called = true) @@ -185,10 +181,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Failed); - - Event(() => Initialized); - During(Initial, When(Initialized) .Try(x => x.Then(instance => instance.Called = true) diff --git a/src/Automatonymous.Tests/FilterExpression_Specs.cs b/src/Automatonymous.Tests/FilterExpression_Specs.cs index f13dc0a..a63079e 100644 --- a/src/Automatonymous.Tests/FilterExpression_Specs.cs +++ b/src/Automatonymous.Tests/FilterExpression_Specs.cs @@ -45,11 +45,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => True); - State(() => False); - - Event(() => Thing); - During(Initial, When(Thing, msg => msg.Condition) .TransitionTo(True), diff --git a/src/Automatonymous.Tests/Group_Specs.cs b/src/Automatonymous.Tests/Group_Specs.cs index d76eb16..ce344c0 100644 --- a/src/Automatonymous.Tests/Group_Specs.cs +++ b/src/Automatonymous.Tests/Group_Specs.cs @@ -77,10 +77,6 @@ public PitStop() { InstanceState(x => x.OverallState); - State(() => BeingServiced); - - Event(() => VehicleArrived); - During(Initial, When(VehicleArrived) .Then((instance, m) => @@ -110,8 +106,6 @@ public FillTank() { InstanceState(x => x.FuelState); - State(() => Filling); - Initially( When(Initial.Enter) .TransitionTo(Filling)); @@ -140,8 +134,6 @@ public CheckOil() { InstanceState(x => x.OilState); - State(() => AddingOil); - Initially( When(Initial.Enter) .TransitionTo(AddingOil)); diff --git a/src/Automatonymous.Tests/InstanceLift_Specs.cs b/src/Automatonymous.Tests/InstanceLift_Specs.cs index a430efc..8f2f309 100644 --- a/src/Automatonymous.Tests/InstanceLift_Specs.cs +++ b/src/Automatonymous.Tests/InstanceLift_Specs.cs @@ -53,10 +53,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Running)); @@ -111,10 +107,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - - Event(() => Initialized); - During(Initial, When(Initialized) .TransitionTo(Running)); diff --git a/src/Automatonymous.Tests/Introspection_Specs.cs b/src/Automatonymous.Tests/Introspection_Specs.cs index 9f269a5..e727250 100644 --- a/src/Automatonymous.Tests/Introspection_Specs.cs +++ b/src/Automatonymous.Tests/Introspection_Specs.cs @@ -89,15 +89,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - Event(() => Hello); - Event(() => YelledAt); - Event(() => Handshake); - Event(() => Ignored); - - State(() => Greeted); - State(() => Loved); - State(() => Pissed); - Initially( When(Hello) .TransitionTo(Greeted)); diff --git a/src/Automatonymous.Tests/Observable_Specs.cs b/src/Automatonymous.Tests/Observable_Specs.cs index 72f07d1..0974786 100644 --- a/src/Automatonymous.Tests/Observable_Specs.cs +++ b/src/Automatonymous.Tests/Observable_Specs.cs @@ -69,11 +69,6 @@ class InstanceStateMachine : { public InstanceStateMachine() { - State(() => Running); - - Event(() => Initialized); - Event(() => Finish); - During(Initial, When(Initialized) .TransitionTo(Running)); diff --git a/src/Automatonymous.Tests/SerializeState_Specs.cs b/src/Automatonymous.Tests/SerializeState_Specs.cs index 0c26c45..c69bfc5 100644 --- a/src/Automatonymous.Tests/SerializeState_Specs.cs +++ b/src/Automatonymous.Tests/SerializeState_Specs.cs @@ -55,11 +55,6 @@ public InstanceStateMachine() { InstanceState(x => x.CurrentState); - State(() => True); - State(() => False); - - Event(() => Thing); - During(Initial, When(Thing, msg => msg.Condition) .TransitionTo(True), diff --git a/src/Automatonymous.Tests/State_Specs.cs b/src/Automatonymous.Tests/State_Specs.cs index b2af85d..9a0606f 100644 --- a/src/Automatonymous.Tests/State_Specs.cs +++ b/src/Automatonymous.Tests/State_Specs.cs @@ -63,8 +63,6 @@ class TestStateMachine : public TestStateMachine() { InstanceState(x => x.CurrentState); - - State(() => Running); } public State Running { get; private set; } @@ -144,9 +142,6 @@ public TestStateMachine() { InstanceState(x => x.AutomatonymousState); - State(() => Running); - Event(() => Started); - Initially( When(Started) .TransitionTo(Running)); diff --git a/src/Automatonymous.Tests/Transition_Specs.cs b/src/Automatonymous.Tests/Transition_Specs.cs index d09023b..515f1e9 100644 --- a/src/Automatonymous.Tests/Transition_Specs.cs +++ b/src/Automatonymous.Tests/Transition_Specs.cs @@ -75,11 +75,6 @@ class InstanceStateMachine : { public InstanceStateMachine() { - State(() => Running); - - Event(() => Initialized); - Event(() => Finish); - During(Initial, When(Initialized) .TransitionTo(Running)); diff --git a/src/Automatonymous.Tests/Visualizer_Specs.cs b/src/Automatonymous.Tests/Visualizer_Specs.cs index b6b6c62..9e11105 100644 --- a/src/Automatonymous.Tests/Visualizer_Specs.cs +++ b/src/Automatonymous.Tests/Visualizer_Specs.cs @@ -88,17 +88,7 @@ class InstanceStateMachine : { public InstanceStateMachine() { - InstanceState(x => x.CurrentState); - - State(() => Running); - State(() => Suspended); - State(() => Failed); - - Event(() => Initialized); - Event(() => Finished); - Event(() => Suspend); - Event(() => Resume); - Event(() => Restart); + InstanceState(x => x.CurrentState); During(Initial, When(Initialized) diff --git a/src/Automatonymous/AutomatonymousStateMachine.cs b/src/Automatonymous/AutomatonymousStateMachine.cs index f97f53f..5ecfd42 100644 --- a/src/Automatonymous/AutomatonymousStateMachine.cs +++ b/src/Automatonymous/AutomatonymousStateMachine.cs @@ -12,364 +12,467 @@ // specific language governing permissions and limitations under the License. namespace Automatonymous { - using System; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; - using System.Reflection; - using Activities; - using Binders; - using Impl; - using Internals.Caching; - using Internals.Extensions; - using Internals.Primitives; - using Taskell; - - - public abstract class AutomatonymousStateMachine : - AcceptStateMachineInspector, - StateMachine - where TInstance : class - { - readonly Cache> _eventCache; - readonly EventRaisedObserver _eventRaisedObserver; - readonly EventRaisingObserver _eventRaisingObserver; - readonly Cache> _stateCache; - readonly Observable> _stateChangedObservable; - StateAccessor _instanceStateAccessor; - - protected AutomatonymousStateMachine() - { - _stateCache = new DictionaryCache>(); - _eventCache = new DictionaryCache>(); - - _stateChangedObservable = new Observable>(); - _eventRaisingObserver = new EventRaisingObserver(_eventCache); - _eventRaisedObserver = new EventRaisedObserver(_eventCache); - - State(() => Initial); - State(() => Final); - - _instanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], - _stateChangedObservable); - } - - public void Accept(StateMachineInspector inspector) - { - Initial.Accept(inspector); - - _stateCache.Each(x => - { - if (Equals(x, Initial) || Equals(x, Final)) - return; - - x.Accept(inspector); - }); - - Final.Accept(inspector); - } - - StateAccessor StateMachine.InstanceStateAccessor - { - get { return _instanceStateAccessor; } - } - - public State Initial { get; private set; } - public State Final { get; private set; } - - State StateMachine.GetState(string name) - { - return _stateCache[name]; - } - - void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event) - { - composer.Execute(() => - { - State state = _instanceStateAccessor.Get(instance); - - State instanceState = _stateCache[state.Name]; - - return composer.ComposeEvent(instance, instanceState, @event); - }); - } - - void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event, TData data) - { - composer.Execute(() => - { - State state = _instanceStateAccessor.Get(instance); - - State instanceState = _stateCache[state.Name]; - - return composer.ComposeEvent(instance, instanceState, @event, data); - }); - } - - public State GetState(string name) - { - return _stateCache[name]; - } - - public IEnumerable States - { - get { return _stateCache; } - } - - Event StateMachine.GetEvent(string name) - { - return _eventCache[name].Event; - } - - public IEnumerable Events - { - get { return _eventCache.Select(x => x.Event); } - } - - public Type InstanceType - { - get { return typeof(TInstance); } - } - - public IEnumerable NextEvents(State state) - { - return _stateCache[state.Name].Events; - } - - public IObservable> StateChanged - { - get { return _stateChangedObservable; } - } - - public IObservable> EventRaising(Event @event) - { - if (!_eventCache.Has(@event.Name)) - throw new ArgumentException("Unknown event: " + @event.Name, "event"); - - return _eventCache[@event.Name].EventRaising; - } - - public IObservable> EventRaised(Event @event) - { - if (!_eventCache.Has(@event.Name)) - throw new ArgumentException("Unknown event: " + @event.Name, "event"); - - return _eventCache[@event.Name].EventRaised; - } - - /// - /// Declares what property holds the TInstance's state on the current instance of the state machine - /// - /// - /// Setting the state accessor more than once will cause the property managed by the state machine to change each time. - /// Please note, the state machine can only manage one property at a given time per instance, - /// and the best practice is to manage one property per machine. - /// - protected void InstanceState(Expression> instanceStateProperty) - { - _instanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, - _stateCache[Initial.Name], _stateChangedObservable); - } - - protected void Event(Expression> propertyExpression) - { - PropertyInfo property = propertyExpression.GetPropertyInfo(); - - string name = property.Name; - - var @event = new SimpleEvent(name); - - property.SetValue(this, @event); - - _eventCache[name] = new StateMachineEvent(@event); - } - - /// - /// Adds a composite event to the state machine. A composite event is triggered when all - /// off the required events have been raised. Note that required events cannot be in the initial - /// state since it would cause extra instances of the state machine to be created - /// - /// The composite event - /// The property in the instance used to track the state of the composite event - /// The events that must be raised before the composite event is raised - protected void Event(Expression> propertyExpression, - Expression> trackingPropertyExpression, - params Event[] events) - { - if (events == null) - throw new ArgumentNullException("events"); - if (events.Length > 31) - throw new ArgumentException("No more than 31 events can be combined into a single event"); - if (events.Length == 0) - throw new ArgumentException("At least one event must be specified for a composite event"); - if (events.Any(x => x == null)) - throw new ArgumentException("One or more events specified has not yet been initialized"); - - PropertyInfo eventProperty = propertyExpression.GetPropertyInfo(); - PropertyInfo trackingPropertyInfo = trackingPropertyExpression.GetPropertyInfo(); - - string name = eventProperty.Name; - - var @event = new SimpleEvent(name); - - eventProperty.SetValue(this, @event); - - _eventCache[name] = new StateMachineEvent(@event); - - var complete = new CompositeEventStatus(Enumerable.Range(0, events.Length) - .Aggregate(0, (current, x) => current | (1 << x))); - - for (int i = 0; i < events.Length; i++) - { - int flag = 1 << i; - - var activity = new CompositeEventActivity(trackingPropertyInfo, flag, complete, - (consumer, instance) => ((StateMachine)this).RaiseEvent(consumer, instance, @event)); - - foreach (var state in _stateCache.Where(x => !Equals(x, Initial))) - { - During(state, - When(events[i]) - .Then(() => activity)); - } - } - } - - protected void Event(Expression>> propertyExpression) - { - PropertyInfo property = propertyExpression.GetPropertyInfo(); - - string name = property.Name; - - var @event = new DataEvent(name); - - property.SetValue(this, @event); - - _eventCache[name] = new StateMachineEvent(@event); - } - - protected void State(Expression> propertyExpression) - { - PropertyInfo property = propertyExpression.GetPropertyInfo(); - - string name = property.Name; - - var state = new StateImpl(name, _eventRaisingObserver, _eventRaisedObserver); - - property.SetValue(this, state); - - _stateCache[name] = state; - } - - protected void During(State state, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state, eventActivities); - } + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + + using Activities; + + using Binders; + + using Impl; + + using Internals.Caching; + using Internals.Extensions; + using Internals.Primitives; + + using Taskell; - protected void During(State state1, State state2, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state1, eventActivities); - BindActivitiesToState(state2, eventActivities); - } - - protected void During(State state1, State state2, State state3, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state1, eventActivities); - BindActivitiesToState(state2, eventActivities); - BindActivitiesToState(state3, eventActivities); - } - - protected void During(State state1, State state2, State state3, State state4, - params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state1, eventActivities); - BindActivitiesToState(state2, eventActivities); - BindActivitiesToState(state3, eventActivities); - BindActivitiesToState(state4, eventActivities); - } - protected void During(IEnumerable states, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + public abstract class AutomatonymousStateMachine + { + private static readonly object PropertyLocker = new object(); + private static readonly IDictionary> Properties = new ConcurrentDictionary>(); + + protected static readonly Type OpenGenericDataEventType = typeof (DataEvent<>); + + // TODO: make this an Extension Method of Type + private static IEnumerable GetInheritenceChain(Type type) + { + var result = new List + { + type + }; + + var b = type.BaseType; + + while (b != null) + { + result.Add(b); + + b = b.BaseType; + } + + return result; + } + + protected static IEnumerable GetProperties(Type type) + { + return GetProperties(type, BindingFlags.Public | BindingFlags.Instance); + } + + protected static IEnumerable GetProperties(Type type, BindingFlags flags) + { + if (Properties.ContainsKey(type) == false) + { + lock (PropertyLocker) + { + if (Properties.ContainsKey(type) == false) + { + var properties = GetInheritenceChain(type) + .SelectMany(x => x.GetProperties(flags | BindingFlags.DeclaredOnly)); + + Properties[type] = properties; + } + } + } + + return Properties[type]; + } + } + + public abstract class AutomatonymousStateMachine : + AutomatonymousStateMachine, + AcceptStateMachineInspector, + StateMachine + where TInstance : class + { + void RegisterEvents() + { + var properties = GetProperties(GetType()) + .Where(x => typeof (Event).IsAssignableFrom(x.PropertyType)); // TODO: this may just need to be equals + + foreach (var property in properties) + { + var name = property.Name; + + Event @event; + + if (property.PropertyType.IsGenericType) + { + var dataEventType = OpenGenericDataEventType.MakeGenericType(property.PropertyType.GetGenericArguments()); + + @event = (Event) Activator.CreateInstance(dataEventType, name); + } + else + { + @event = new SimpleEvent(name); + } + + _eventCache[name] = new StateMachineEvent(@event); + + property.SetValue(this, @event); + } + } + + void RegisterStates() + { + var properties = GetProperties(GetType()) + .Where(x => typeof (State).IsAssignableFrom(x.PropertyType)); // TODO: this may just need to be equals + + foreach (var property in properties) + { + var name = property.Name; + + State state = new StateImpl(name, _eventRaisingObserver, _eventRaisedObserver); + + _stateCache[name] = state; + + property.SetValue(this, state); + } + } + + readonly Cache> _eventCache; + readonly EventRaisedObserver _eventRaisedObserver; + readonly EventRaisingObserver _eventRaisingObserver; + readonly Cache> _stateCache; + readonly Observable> _stateChangedObservable; + StateAccessor _instanceStateAccessor; + + protected AutomatonymousStateMachine() + { + _stateCache = new DictionaryCache>(); + _eventCache = new DictionaryCache>(); + + _stateChangedObservable = new Observable>(); + _eventRaisingObserver = new EventRaisingObserver(_eventCache); + _eventRaisedObserver = new EventRaisedObserver(_eventCache); + + RegisterStates(); + RegisterEvents(); + + _instanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], _stateChangedObservable); + } + + public void Accept(StateMachineInspector inspector) + { + Initial.Accept(inspector); + + _stateCache.Each(x => + { + if (Equals(x, Initial) || Equals(x, Final)) + return; + + x.Accept(inspector); + }); + + Final.Accept(inspector); + } + + StateAccessor StateMachine.InstanceStateAccessor + { + get { return _instanceStateAccessor; } + } + + public State Initial { get; protected set; } + public State Final { get; private set; } + + State StateMachine.GetState(string name) + { + return _stateCache[name]; + } + + void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event) + { + composer.Execute(() => + { + State state = _instanceStateAccessor.Get(instance); + + State instanceState = _stateCache[state.Name]; + + return composer.ComposeEvent(instance, instanceState, @event); + }); + } + + void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event, TData data) + { + composer.Execute(() => + { + State state = _instanceStateAccessor.Get(instance); + + State instanceState = _stateCache[state.Name]; + + return composer.ComposeEvent(instance, instanceState, @event, data); + }); + } + + public State GetState(string name) + { + return _stateCache[name]; + } + + public IEnumerable States + { + get { return _stateCache; } + } + + Event StateMachine.GetEvent(string name) + { + return _eventCache[name].Event; + } + + public IEnumerable Events + { + get { return _eventCache.Select(x => x.Event); } + } + + public Type InstanceType + { + get { return typeof(TInstance); } + } + + public IEnumerable NextEvents(State state) + { + return _stateCache[state.Name].Events; + } + + public IObservable> StateChanged + { + get { return _stateChangedObservable; } + } + + public IObservable> EventRaising(Event @event) + { + if (!_eventCache.Has(@event.Name)) + throw new ArgumentException("Unknown event: " + @event.Name, "event"); + + return _eventCache[@event.Name].EventRaising; + } + + public IObservable> EventRaised(Event @event) + { + if (!_eventCache.Has(@event.Name)) + throw new ArgumentException("Unknown event: " + @event.Name, "event"); + + return _eventCache[@event.Name].EventRaised; + } + + /// + /// Declares what property holds the TInstance's state on the current instance of the state machine + /// + /// + /// Setting the state accessor more than once will cause the property managed by the state machine to change each time. + /// Please note, the state machine can only manage one property at a given time per instance, + /// and the best practice is to manage one property per machine. + /// + protected void InstanceState(Expression> instanceStateProperty) + { + _instanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, + _stateCache[Initial.Name], _stateChangedObservable); + } + + //protected void Event(Expression> propertyExpression) + //{ + // PropertyInfo property = propertyExpression.GetPropertyInfo(); + + // string name = property.Name; + + // var @event = new SimpleEvent(name); + + // property.SetValue(this, @event); + + // _eventCache[name] = new StateMachineEvent(@event); + //} + + /// + /// Adds a composite event to the state machine. A composite event is triggered when all + /// off the required events have been raised. Note that required events cannot be in the initial + /// state since it would cause extra instances of the state machine to be created + /// + /// The composite event + /// The property in the instance used to track the state of the composite event + /// The events that must be raised before the composite event is raised + protected void Event(Expression> propertyExpression, + Expression> trackingPropertyExpression, + params Event[] events) + { + if (events == null) + throw new ArgumentNullException("events"); + if (events.Length > 31) + throw new ArgumentException("No more than 31 events can be combined into a single event"); + if (events.Length == 0) + throw new ArgumentException("At least one event must be specified for a composite event"); + if (events.Any(x => x == null)) + throw new ArgumentException("One or more events specified has not yet been initialized"); + + PropertyInfo eventProperty = propertyExpression.GetPropertyInfo(); + PropertyInfo trackingPropertyInfo = trackingPropertyExpression.GetPropertyInfo(); + + string name = eventProperty.Name; + + var @event = new SimpleEvent(name); + + eventProperty.SetValue(this, @event); + + _eventCache[name] = new StateMachineEvent(@event); + + var complete = new CompositeEventStatus(Enumerable.Range(0, events.Length) + .Aggregate(0, (current, x) => current | (1 << x))); + + for (int i = 0; i < events.Length; i++) + { + int flag = 1 << i; + + var activity = new CompositeEventActivity(trackingPropertyInfo, flag, complete, + (consumer, instance) => ((StateMachine)this).RaiseEvent(consumer, instance, @event)); + + foreach (var state in _stateCache.Where(x => !Equals(x, Initial))) + { + During(state, + When(events[i]) + .Then(() => activity)); + } + } + } + + //protected void Event(Expression>> propertyExpression) + //{ + // PropertyInfo property = propertyExpression.GetPropertyInfo(); + + // string name = property.Name; + + // var @event = new DataEvent(name); + + // property.SetValue(this, @event); + + // _eventCache[name] = new StateMachineEvent(@event); + //} + + //protected void State(Expression> propertyExpression) + //{ + // PropertyInfo property = propertyExpression.GetPropertyInfo(); + + // string name = property.Name; + + // var state = new StateImpl(name, _eventRaisingObserver, _eventRaisedObserver); + + // property.SetValue(this, state); + + // _stateCache[name] = state; + //} + + protected void During(State state, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state, eventActivities); + } + + protected void During(State state1, State state2, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state1, eventActivities); + BindActivitiesToState(state2, eventActivities); + } + + protected void During(State state1, State state2, State state3, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state1, eventActivities); + BindActivitiesToState(state2, eventActivities); + BindActivitiesToState(state3, eventActivities); + } + + protected void During(State state1, State state2, State state3, State state4, + params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state1, eventActivities); + BindActivitiesToState(state2, eventActivities); + BindActivitiesToState(state3, eventActivities); + BindActivitiesToState(state4, eventActivities); + } - foreach (var state in states) - { - BindActivitiesToState(state, eventActivities); - } - } + protected void During(IEnumerable states, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - static void BindActivitiesToState(State state, IEnumerable> eventActivities) - { - State activityState = state.For(); + foreach (var state in states) + { + BindActivitiesToState(state, eventActivities); + } + } - foreach (var activity in eventActivities) - activityState.Bind(activity); - } + static void BindActivitiesToState(State state, IEnumerable> eventActivities) + { + State activityState = state.For(); - protected void Initially(params IEnumerable>[] activities) - { - During(Initial, activities); - } + foreach (var activity in eventActivities) + activityState.Bind(activity); + } - protected void DuringAny(params IEnumerable>[] activities) - { - IEnumerable> states = _stateCache.Where(x => !Equals(x, Initial) && !Equals(x, Final)); + protected void Initially(params IEnumerable>[] activities) + { + During(Initial, activities); + } - // we only add DuringAny event handlers to non-initial and non-final states to avoid - // reviving finalized state machine instances or creating new ones accidentally. - foreach (var state in states) - During(state, activities); - - BindTransitionEvents(Initial, activities); - BindTransitionEvents(Final, activities); - } + protected void DuringAny(params IEnumerable>[] activities) + { + IEnumerable> states = _stateCache.Where(x => !Equals(x, Initial) && !Equals(x, Final)); - void BindTransitionEvents(State state, IEnumerable>> activities) - { - IEnumerable> eventActivities = activities - .SelectMany(activity => activity.Where(x => IsTransitionEvent(state, x.Event))); + // we only add DuringAny event handlers to non-initial and non-final states to avoid + // reviving finalized state machine instances or creating new ones accidentally. + foreach (var state in states) + During(state, activities); + + BindTransitionEvents(Initial, activities); + BindTransitionEvents(Final, activities); + } - foreach (var eventActivity in eventActivities) - During(state, new[] {eventActivity}); - } + void BindTransitionEvents(State state, IEnumerable>> activities) + { + IEnumerable> eventActivities = activities + .SelectMany(activity => activity.Where(x => IsTransitionEvent(state, x.Event))); - bool IsTransitionEvent(State state, Event eevent) - { - return Equals(eevent, state.Enter) || Equals(eevent, state.BeforeEnter) - || Equals(eevent, state.AfterLeave) || Equals(eevent, state.Leave); - } - - protected void Finally(Func, EventActivityBinder> activityCallback) - { - EventActivityBinder binder = When(Final.Enter); - - binder = activityCallback(binder); - - DuringAny(binder); - } - - protected EventActivityBinder When(Event @event) - { - return new SimpleEventActivityBinder(this, @event); - } - - protected EventActivityBinder When(Event @event) - { - return new DataEventActivityBinder(this, @event); - } - - protected EventActivityBinder When(Event @event, - Expression> filterExpression) - { - return new DataEventActivityBinder(this, @event, filterExpression); - } - } -} \ No newline at end of file + foreach (var eventActivity in eventActivities) + During(state, new[] { eventActivity }); + } + + bool IsTransitionEvent(State state, Event eevent) + { + return Equals(eevent, state.Enter) || Equals(eevent, state.BeforeEnter) + || Equals(eevent, state.AfterLeave) || Equals(eevent, state.Leave); + } + + protected void Finally(Func, EventActivityBinder> activityCallback) + { + EventActivityBinder binder = When(Final.Enter); + + binder = activityCallback(binder); + + DuringAny(binder); + } + + protected EventActivityBinder When(Event @event) + { + return new SimpleEventActivityBinder(this, @event); + } + + protected EventActivityBinder When(Event @event) + { + return new DataEventActivityBinder(this, @event); + } + + protected EventActivityBinder When(Event @event, + Expression> filterExpression) + { + return new DataEventActivityBinder(this, @event, filterExpression); + } + } +} diff --git a/src/Automatonymous/Event.cs b/src/Automatonymous/Event.cs index 7519eab..68548fe 100644 --- a/src/Automatonymous/Event.cs +++ b/src/Automatonymous/Event.cs @@ -27,4 +27,4 @@ public interface Event : Event { } -} \ No newline at end of file +} diff --git a/src/Automatonymous/State.cs b/src/Automatonymous/State.cs index eba6880..9900b97 100644 --- a/src/Automatonymous/State.cs +++ b/src/Automatonymous/State.cs @@ -53,4 +53,4 @@ public interface State : void Bind(EventActivity activity); } -} \ No newline at end of file +} diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs index d80face..e0af574 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs @@ -94,11 +94,7 @@ class TestStateMachine : public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Waiting); - Event(() => Start); - Event(() => First); - Event(() => Second); Event(() => Third, x => x.CompositeStatus, First, Second); Initially( diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/Fault_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/Fault_Specs.cs index a7d00e8..cd8db1b 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/Fault_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/Fault_Specs.cs @@ -109,15 +109,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - State(() => WaitingToStart); - State(() => FailedToStart); - - Event(() => Started); - Event(() => Initialized); - Event(() => Created); - Event(() => StartFaulted); - Initially( When(Started) .Then(instance => { throw new NotSupportedException("This is expected, but nonetheless exceptional"); }) diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/PublishRequest_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/PublishRequest_Specs.cs index f702dd6..0afc185 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/PublishRequest_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/PublishRequest_Specs.cs @@ -74,9 +74,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - Event(() => Started); - Initially( When(Started) .PublishRequest((instance, context) => new RequestAuthorization diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/Publish_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/Publish_Specs.cs index cde0661..4bbab1e 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/Publish_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/Publish_Specs.cs @@ -75,9 +75,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - Event(() => Started); - Initially( When(Started) .Publish((instance, msg) => new StartupComplete diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/RemoveWhen_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/RemoveWhen_Specs.cs index 58a23b3..0fd25f2 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/RemoveWhen_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/RemoveWhen_Specs.cs @@ -101,12 +101,8 @@ class TestStateMachine : AutomatonymousStateMachine { public TestStateMachine() - { - InstanceState(x => x.CurrentState); - - State(() => Running); - Event(() => Started); - Event(() => Stopped); + { + InstanceState(x => x.CurrentState); Initially( When(Started) diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/Respond_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/Respond_Specs.cs index b4436ae..175b778 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/Respond_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/Respond_Specs.cs @@ -75,9 +75,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - Event(() => Started); - Initially( When(Started) .Respond(new StartupComplete()) diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs index 16e4d24..c4e2bd7 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs @@ -100,11 +100,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - Event(() => Started); - Event(() => Stopped); - Event(() => ShouldNotBind); - Initially( When(Started) .TransitionTo(Running)); diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/Testing_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/Testing_Specs.cs index 92486f3..4b99f83 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/Testing_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/Testing_Specs.cs @@ -116,11 +116,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - Event(() => Started); - Event(() => Stopped); - Event(() => ShouldNotBind); - Initially( When(Started) .TransitionTo(Running)); diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/UncorrelatedMessage_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/UncorrelatedMessage_Specs.cs index 3f153ac..431f702 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/UncorrelatedMessage_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/UncorrelatedMessage_Specs.cs @@ -110,10 +110,6 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - State(() => Running); - Event(() => Started); - Event(() => CheckStatus); - Initially( When(Started) .Then((i, d) => i.ServiceName = d.ServiceName) diff --git a/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs b/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs index 9cf0ccd..96c1242 100644 --- a/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs +++ b/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs @@ -176,11 +176,6 @@ public SuperShopper() { InstanceState(x => x.CurrentState); - State(() => OnTheWayToTheStore); - - Event(() => ExitFrontDoor); - Event(() => GotHitByCar); - Event(() => EndOfTheWorld, x => x.Everything, ExitFrontDoor, GotHitByCar); Initially( From 74477fbec1c0e2b5f8a61f3cfc2b9821667a471b Mon Sep 17 00:00:00 2001 From: Chris Block Date: Sat, 18 Oct 2014 16:02:54 -0500 Subject: [PATCH 2/7] Add tests for the reflection based event and state registration --- .../Automatonymous.Tests.csproj | 1 + .../AutomatonymousStateMachine_Specs.cs | 88 +++++++++++++++++++ src/Automatonymous.Tests/Combine_Specs.cs | 4 +- .../AutomatonymousStateMachine.cs | 67 ++++---------- src/Automatonymous/CompositeEventStatus.cs | 2 +- .../CompositeEvent_Specs.cs | 2 +- .../SimpleStateMachine_Specs.cs | 2 +- .../Vanilla_Specs.cs | 2 +- 8 files changed, 111 insertions(+), 57 deletions(-) create mode 100644 src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs diff --git a/src/Automatonymous.Tests/Automatonymous.Tests.csproj b/src/Automatonymous.Tests/Automatonymous.Tests.csproj index 3d19f09..bba89d1 100644 --- a/src/Automatonymous.Tests/Automatonymous.Tests.csproj +++ b/src/Automatonymous.Tests/Automatonymous.Tests.csproj @@ -53,6 +53,7 @@ SolutionVersion.cs + diff --git a/src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs b/src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs new file mode 100644 index 0000000..e37a306 --- /dev/null +++ b/src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs @@ -0,0 +1,88 @@ +// Copyright 2011 Chris Patterson, Dru Sellers +// +// Licensed under the Apache License, Version 2.0 (the "License"); you may not use +// this file except in compliance with the License. You may obtain a copy of the +// License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software distributed +// under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR +// CONDITIONS OF ANY KIND, either express or implied. See the License for the +// specific language governing permissions and limitations under the License. +namespace Automatonymous.Tests +{ + using System.Linq; + + using NUnit.Framework; + + + [TestFixture] + public class Using_a_simple_state_machine + { + [Test] + public void Should_register_all_state_properties() + { + var stateMachine = new TestStateMachine(); + + var states = stateMachine.States.ToList(); + + Assert.That(states, Has.Count.EqualTo(4)); + Assert.That(states, Contains.Item(stateMachine.Initial)); + Assert.That(states, Contains.Item(stateMachine.Final)); + Assert.That(states, Contains.Item(stateMachine.ThisIsAState)); + Assert.That(states, Contains.Item(stateMachine.ThisIsAlsoAState)); + } + + [Test] + public void Should_initialize_all_state_properties() + { + var stateMachine = new TestStateMachine(); + + Assert.That(stateMachine.Initial, Is.Not.Null); + Assert.That(stateMachine.Final, Is.Not.Null); + Assert.That(stateMachine.ThisIsAState, Is.Not.Null); + Assert.That(stateMachine.ThisIsAlsoAState, Is.Not.Null); + } + + [Test] + public void Should_register_all_event_properties() + { + var stateMachine = new TestStateMachine(); + + var events = stateMachine.Events.ToList(); + + Assert.That(events, Has.Count.EqualTo(2)); + Assert.That(events, Contains.Item(stateMachine.ThisIsASimpleEvent)); + Assert.That(events, Contains.Item(stateMachine.ThisIsAnEventConsumingData)); + } + + [Test] + public void Should_initialize_all_event_properties() + { + var stateMachine = new TestStateMachine(); + + Assert.That(stateMachine.ThisIsASimpleEvent, Is.Not.Null); + Assert.That(stateMachine.ThisIsAnEventConsumingData, Is.Not.Null); + } + + + class Instance + { + } + + + class EventData + { + } + + + class TestStateMachine : AutomatonymousStateMachine + { + public State ThisIsAState { get; private set; } + public State ThisIsAlsoAState { get; private set; } + public Event ThisIsASimpleEvent { get; private set; } + public Event ThisIsAnEventConsumingData { get; private set; } + } + } +} diff --git a/src/Automatonymous.Tests/Combine_Specs.cs b/src/Automatonymous.Tests/Combine_Specs.cs index b70860b..d82941f 100644 --- a/src/Automatonymous.Tests/Combine_Specs.cs +++ b/src/Automatonymous.Tests/Combine_Specs.cs @@ -73,7 +73,7 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - Event(() => Third, x => x.CompositeStatus, First, Second); + ComposeEvent(Third, x => x.CompositeStatus, First, Second); Initially( When(Start) @@ -93,4 +93,4 @@ public TestStateMachine() public Event Third { get; private set; } } } -} \ No newline at end of file +} diff --git a/src/Automatonymous/AutomatonymousStateMachine.cs b/src/Automatonymous/AutomatonymousStateMachine.cs index 5ecfd42..5b1909f 100644 --- a/src/Automatonymous/AutomatonymousStateMachine.cs +++ b/src/Automatonymous/AutomatonymousStateMachine.cs @@ -274,51 +274,42 @@ protected void InstanceState(Expression> instanceStatePro _stateCache[Initial.Name], _stateChangedObservable); } - //protected void Event(Expression> propertyExpression) - //{ - // PropertyInfo property = propertyExpression.GetPropertyInfo(); - - // string name = property.Name; - - // var @event = new SimpleEvent(name); - - // property.SetValue(this, @event); - - // _eventCache[name] = new StateMachineEvent(@event); - //} - /// /// Adds a composite event to the state machine. A composite event is triggered when all /// off the required events have been raised. Note that required events cannot be in the initial /// state since it would cause extra instances of the state machine to be created /// - /// The composite event + /// The composite event /// The property in the instance used to track the state of the composite event /// The events that must be raised before the composite event is raised - protected void Event(Expression> propertyExpression, + protected void ComposeEvent(Event @event, Expression> trackingPropertyExpression, params Event[] events) { + // TODO: check @event for null??? + if (events == null) + { throw new ArgumentNullException("events"); + } + if (events.Length > 31) + { throw new ArgumentException("No more than 31 events can be combined into a single event"); + } + if (events.Length == 0) + { throw new ArgumentException("At least one event must be specified for a composite event"); + } + if (events.Any(x => x == null)) + { throw new ArgumentException("One or more events specified has not yet been initialized"); + } - PropertyInfo eventProperty = propertyExpression.GetPropertyInfo(); PropertyInfo trackingPropertyInfo = trackingPropertyExpression.GetPropertyInfo(); - string name = eventProperty.Name; - - var @event = new SimpleEvent(name); - - eventProperty.SetValue(this, @event); - - _eventCache[name] = new StateMachineEvent(@event); - var complete = new CompositeEventStatus(Enumerable.Range(0, events.Length) .Aggregate(0, (current, x) => current | (1 << x))); @@ -327,7 +318,7 @@ protected void Event(Expression> propertyExpression, int flag = 1 << i; var activity = new CompositeEventActivity(trackingPropertyInfo, flag, complete, - (consumer, instance) => ((StateMachine)this).RaiseEvent(consumer, instance, @event)); + (consumer, instance) => ((StateMachine) this).RaiseEvent(consumer, instance, @event)); foreach (var state in _stateCache.Where(x => !Equals(x, Initial))) { @@ -338,32 +329,6 @@ protected void Event(Expression> propertyExpression, } } - //protected void Event(Expression>> propertyExpression) - //{ - // PropertyInfo property = propertyExpression.GetPropertyInfo(); - - // string name = property.Name; - - // var @event = new DataEvent(name); - - // property.SetValue(this, @event); - - // _eventCache[name] = new StateMachineEvent(@event); - //} - - //protected void State(Expression> propertyExpression) - //{ - // PropertyInfo property = propertyExpression.GetPropertyInfo(); - - // string name = property.Name; - - // var state = new StateImpl(name, _eventRaisingObserver, _eventRaisedObserver); - - // property.SetValue(this, state); - - // _stateCache[name] = state; - //} - protected void During(State state, params IEnumerable>[] activities) { EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); diff --git a/src/Automatonymous/CompositeEventStatus.cs b/src/Automatonymous/CompositeEventStatus.cs index 16242c0..9330343 100644 --- a/src/Automatonymous/CompositeEventStatus.cs +++ b/src/Automatonymous/CompositeEventStatus.cs @@ -61,4 +61,4 @@ public void Set(int flag) _bits |= flag; } } -} \ No newline at end of file +} diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs index e0af574..37d4ab4 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/CompositeEvent_Specs.cs @@ -95,7 +95,7 @@ public TestStateMachine() { InstanceState(x => x.CurrentState); - Event(() => Third, x => x.CompositeStatus, First, Second); + ComposeEvent(Third, x => x.CompositeStatus, First, Second); Initially( When(Start) diff --git a/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs b/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs index c4e2bd7..9f660df 100644 --- a/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs +++ b/src/MassTransit/MassTransit.AutomatonymousTests/SimpleStateMachine_Specs.cs @@ -129,4 +129,4 @@ class Stop : public Guid CorrelationId { get; set; } } } -} \ No newline at end of file +} diff --git a/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs b/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs index 96c1242..7bf324f 100644 --- a/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs +++ b/src/NHibernate.AutomatonymousTests/Vanilla_Specs.cs @@ -176,7 +176,7 @@ public SuperShopper() { InstanceState(x => x.CurrentState); - Event(() => EndOfTheWorld, x => x.Everything, ExitFrontDoor, GotHitByCar); + ComposeEvent(EndOfTheWorld, x => x.Everything, ExitFrontDoor, GotHitByCar); Initially( When(ExitFrontDoor) From 564d9a22b5583f53c5fd58997ef7b42e373b4fa5 Mon Sep 17 00:00:00 2001 From: Chris Block Date: Sat, 18 Oct 2014 16:43:09 -0500 Subject: [PATCH 3/7] Remove comments from rakefile and fix spelling error. --- rakefile.rb | 20 - .../AutomatonymousStateMachine.cs | 856 +++++++++--------- 2 files changed, 428 insertions(+), 448 deletions(-) diff --git a/rakefile.rb b/rakefile.rb index c5381d3..0ea8e82 100644 --- a/rakefile.rb +++ b/rakefile.rb @@ -45,26 +45,6 @@ asm.namespaces "System", "System.Reflection", "System.Runtime.InteropServices" end -#desc "Update the common version information for the build. You can call this task without building." -#asmver :global_version do |asm| -# # Assembly file config -# asm.file_path = 'src/SolutionVersion.cs' -# -# #asm.namespaces "System", "System.Reflection", "System.Runtime.InteropServices" -# -# asm.attributes assembly_description: "Automatonymous is an open source state machine library.", -# assembly_version: FORMAL_VERSION, -# assembly_file_version: FORMAL_VERSION, -# assembly_informational_version: "#{BUILD_VERSION}", -# assembly_copyright: COPYRIGHT, -# assembly_product: PRODUCT, -# assembly_title: PRODUCT, -# com_visible: false, -# CLS_compliant: true -# -# #asm.out = StringIO.new -#end - desc "Prepares the working directory for a new build" task :clean do FileUtils.rm_rf props[:output] diff --git a/src/Automatonymous/AutomatonymousStateMachine.cs b/src/Automatonymous/AutomatonymousStateMachine.cs index 5b1909f..fe7afcb 100644 --- a/src/Automatonymous/AutomatonymousStateMachine.cs +++ b/src/Automatonymous/AutomatonymousStateMachine.cs @@ -12,432 +12,432 @@ // specific language governing permissions and limitations under the License. namespace Automatonymous { - using System; - using System.Collections.Concurrent; - using System.Collections.Generic; - using System.Linq; - using System.Linq.Expressions; - using System.Reflection; - - using Activities; - - using Binders; - - using Impl; - - using Internals.Caching; - using Internals.Extensions; - using Internals.Primitives; - - using Taskell; - - - public abstract class AutomatonymousStateMachine - { - private static readonly object PropertyLocker = new object(); - private static readonly IDictionary> Properties = new ConcurrentDictionary>(); - - protected static readonly Type OpenGenericDataEventType = typeof (DataEvent<>); - - // TODO: make this an Extension Method of Type - private static IEnumerable GetInheritenceChain(Type type) - { - var result = new List - { - type - }; - - var b = type.BaseType; - - while (b != null) - { - result.Add(b); - - b = b.BaseType; - } - - return result; - } - - protected static IEnumerable GetProperties(Type type) - { - return GetProperties(type, BindingFlags.Public | BindingFlags.Instance); - } - - protected static IEnumerable GetProperties(Type type, BindingFlags flags) - { - if (Properties.ContainsKey(type) == false) - { - lock (PropertyLocker) - { - if (Properties.ContainsKey(type) == false) - { - var properties = GetInheritenceChain(type) - .SelectMany(x => x.GetProperties(flags | BindingFlags.DeclaredOnly)); - - Properties[type] = properties; - } - } - } - - return Properties[type]; - } - } - - public abstract class AutomatonymousStateMachine : - AutomatonymousStateMachine, - AcceptStateMachineInspector, - StateMachine - where TInstance : class - { - void RegisterEvents() - { - var properties = GetProperties(GetType()) - .Where(x => typeof (Event).IsAssignableFrom(x.PropertyType)); // TODO: this may just need to be equals - - foreach (var property in properties) - { - var name = property.Name; - - Event @event; - - if (property.PropertyType.IsGenericType) - { - var dataEventType = OpenGenericDataEventType.MakeGenericType(property.PropertyType.GetGenericArguments()); - - @event = (Event) Activator.CreateInstance(dataEventType, name); - } - else - { - @event = new SimpleEvent(name); - } - - _eventCache[name] = new StateMachineEvent(@event); - - property.SetValue(this, @event); - } - } - - void RegisterStates() - { - var properties = GetProperties(GetType()) - .Where(x => typeof (State).IsAssignableFrom(x.PropertyType)); // TODO: this may just need to be equals - - foreach (var property in properties) - { - var name = property.Name; - - State state = new StateImpl(name, _eventRaisingObserver, _eventRaisedObserver); - - _stateCache[name] = state; - - property.SetValue(this, state); - } - } - - readonly Cache> _eventCache; - readonly EventRaisedObserver _eventRaisedObserver; - readonly EventRaisingObserver _eventRaisingObserver; - readonly Cache> _stateCache; - readonly Observable> _stateChangedObservable; - StateAccessor _instanceStateAccessor; - - protected AutomatonymousStateMachine() - { - _stateCache = new DictionaryCache>(); - _eventCache = new DictionaryCache>(); - - _stateChangedObservable = new Observable>(); - _eventRaisingObserver = new EventRaisingObserver(_eventCache); - _eventRaisedObserver = new EventRaisedObserver(_eventCache); - - RegisterStates(); - RegisterEvents(); - - _instanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], _stateChangedObservable); - } - - public void Accept(StateMachineInspector inspector) - { - Initial.Accept(inspector); - - _stateCache.Each(x => - { - if (Equals(x, Initial) || Equals(x, Final)) - return; - - x.Accept(inspector); - }); - - Final.Accept(inspector); - } - - StateAccessor StateMachine.InstanceStateAccessor - { - get { return _instanceStateAccessor; } - } - - public State Initial { get; protected set; } - public State Final { get; private set; } - - State StateMachine.GetState(string name) - { - return _stateCache[name]; - } - - void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event) - { - composer.Execute(() => - { - State state = _instanceStateAccessor.Get(instance); - - State instanceState = _stateCache[state.Name]; - - return composer.ComposeEvent(instance, instanceState, @event); - }); - } - - void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event, TData data) - { - composer.Execute(() => - { - State state = _instanceStateAccessor.Get(instance); - - State instanceState = _stateCache[state.Name]; - - return composer.ComposeEvent(instance, instanceState, @event, data); - }); - } - - public State GetState(string name) - { - return _stateCache[name]; - } - - public IEnumerable States - { - get { return _stateCache; } - } - - Event StateMachine.GetEvent(string name) - { - return _eventCache[name].Event; - } - - public IEnumerable Events - { - get { return _eventCache.Select(x => x.Event); } - } - - public Type InstanceType - { - get { return typeof(TInstance); } - } - - public IEnumerable NextEvents(State state) - { - return _stateCache[state.Name].Events; - } - - public IObservable> StateChanged - { - get { return _stateChangedObservable; } - } - - public IObservable> EventRaising(Event @event) - { - if (!_eventCache.Has(@event.Name)) - throw new ArgumentException("Unknown event: " + @event.Name, "event"); - - return _eventCache[@event.Name].EventRaising; - } - - public IObservable> EventRaised(Event @event) - { - if (!_eventCache.Has(@event.Name)) - throw new ArgumentException("Unknown event: " + @event.Name, "event"); - - return _eventCache[@event.Name].EventRaised; - } - - /// - /// Declares what property holds the TInstance's state on the current instance of the state machine - /// - /// - /// Setting the state accessor more than once will cause the property managed by the state machine to change each time. - /// Please note, the state machine can only manage one property at a given time per instance, - /// and the best practice is to manage one property per machine. - /// - protected void InstanceState(Expression> instanceStateProperty) - { - _instanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, - _stateCache[Initial.Name], _stateChangedObservable); - } - - /// - /// Adds a composite event to the state machine. A composite event is triggered when all - /// off the required events have been raised. Note that required events cannot be in the initial - /// state since it would cause extra instances of the state machine to be created - /// - /// The composite event - /// The property in the instance used to track the state of the composite event - /// The events that must be raised before the composite event is raised - protected void ComposeEvent(Event @event, - Expression> trackingPropertyExpression, - params Event[] events) - { - // TODO: check @event for null??? - - if (events == null) - { - throw new ArgumentNullException("events"); - } - - if (events.Length > 31) - { - throw new ArgumentException("No more than 31 events can be combined into a single event"); - } - - if (events.Length == 0) - { - throw new ArgumentException("At least one event must be specified for a composite event"); - } - - if (events.Any(x => x == null)) - { - throw new ArgumentException("One or more events specified has not yet been initialized"); - } - - PropertyInfo trackingPropertyInfo = trackingPropertyExpression.GetPropertyInfo(); - - var complete = new CompositeEventStatus(Enumerable.Range(0, events.Length) - .Aggregate(0, (current, x) => current | (1 << x))); - - for (int i = 0; i < events.Length; i++) - { - int flag = 1 << i; - - var activity = new CompositeEventActivity(trackingPropertyInfo, flag, complete, - (consumer, instance) => ((StateMachine) this).RaiseEvent(consumer, instance, @event)); - - foreach (var state in _stateCache.Where(x => !Equals(x, Initial))) - { - During(state, - When(events[i]) - .Then(() => activity)); - } - } - } - - protected void During(State state, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state, eventActivities); - } - - protected void During(State state1, State state2, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state1, eventActivities); - BindActivitiesToState(state2, eventActivities); - } - - protected void During(State state1, State state2, State state3, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state1, eventActivities); - BindActivitiesToState(state2, eventActivities); - BindActivitiesToState(state3, eventActivities); - } - - protected void During(State state1, State state2, State state3, State state4, - params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - BindActivitiesToState(state1, eventActivities); - BindActivitiesToState(state2, eventActivities); - BindActivitiesToState(state3, eventActivities); - BindActivitiesToState(state4, eventActivities); - } - - protected void During(IEnumerable states, params IEnumerable>[] activities) - { - EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); - - foreach (var state in states) - { - BindActivitiesToState(state, eventActivities); - } - } - - static void BindActivitiesToState(State state, IEnumerable> eventActivities) - { - State activityState = state.For(); - - foreach (var activity in eventActivities) - activityState.Bind(activity); - } - - protected void Initially(params IEnumerable>[] activities) - { - During(Initial, activities); - } - - protected void DuringAny(params IEnumerable>[] activities) - { - IEnumerable> states = _stateCache.Where(x => !Equals(x, Initial) && !Equals(x, Final)); - - // we only add DuringAny event handlers to non-initial and non-final states to avoid - // reviving finalized state machine instances or creating new ones accidentally. - foreach (var state in states) - During(state, activities); - - BindTransitionEvents(Initial, activities); - BindTransitionEvents(Final, activities); - } - - void BindTransitionEvents(State state, IEnumerable>> activities) - { - IEnumerable> eventActivities = activities - .SelectMany(activity => activity.Where(x => IsTransitionEvent(state, x.Event))); - - foreach (var eventActivity in eventActivities) - During(state, new[] { eventActivity }); - } - - bool IsTransitionEvent(State state, Event eevent) - { - return Equals(eevent, state.Enter) || Equals(eevent, state.BeforeEnter) - || Equals(eevent, state.AfterLeave) || Equals(eevent, state.Leave); - } - - protected void Finally(Func, EventActivityBinder> activityCallback) - { - EventActivityBinder binder = When(Final.Enter); - - binder = activityCallback(binder); - - DuringAny(binder); - } - - protected EventActivityBinder When(Event @event) - { - return new SimpleEventActivityBinder(this, @event); - } - - protected EventActivityBinder When(Event @event) - { - return new DataEventActivityBinder(this, @event); - } - - protected EventActivityBinder When(Event @event, - Expression> filterExpression) - { - return new DataEventActivityBinder(this, @event, filterExpression); - } - } + using System; + using System.Collections.Concurrent; + using System.Collections.Generic; + using System.Linq; + using System.Linq.Expressions; + using System.Reflection; + + using Activities; + + using Binders; + + using Impl; + + using Internals.Caching; + using Internals.Extensions; + using Internals.Primitives; + + using Taskell; + + + public abstract class AutomatonymousStateMachine + { + private static readonly object PropertyLocker = new object(); + private static readonly IDictionary> Properties = new ConcurrentDictionary>(); + + protected static readonly Type OpenGenericDataEventType = typeof (DataEvent<>); + + // TODO: make this an Extension Method of Type + private static IEnumerable GetInheritanceChain(Type type) + { + var result = new List + { + type + }; + + var b = type.BaseType; + + while (b != null) + { + result.Add(b); + + b = b.BaseType; + } + + return result; + } + + protected static IEnumerable GetProperties(Type type) + { + return GetProperties(type, BindingFlags.Public | BindingFlags.Instance); + } + + protected static IEnumerable GetProperties(Type type, BindingFlags flags) + { + if (Properties.ContainsKey(type) == false) + { + lock (PropertyLocker) + { + if (Properties.ContainsKey(type) == false) + { + var properties = GetInheritanceChain(type) + .SelectMany(x => x.GetProperties(flags | BindingFlags.DeclaredOnly)); + + Properties[type] = properties; + } + } + } + + return Properties[type]; + } + } + + public abstract class AutomatonymousStateMachine : + AutomatonymousStateMachine, + AcceptStateMachineInspector, + StateMachine + where TInstance : class + { + readonly Cache> _eventCache; + readonly EventRaisedObserver _eventRaisedObserver; + readonly EventRaisingObserver _eventRaisingObserver; + readonly Cache> _stateCache; + readonly Observable> _stateChangedObservable; + StateAccessor _instanceStateAccessor; + + protected AutomatonymousStateMachine() + { + _stateCache = new DictionaryCache>(); + _eventCache = new DictionaryCache>(); + + _stateChangedObservable = new Observable>(); + _eventRaisingObserver = new EventRaisingObserver(_eventCache); + _eventRaisedObserver = new EventRaisedObserver(_eventCache); + + RegisterStates(); + RegisterEvents(); + + _instanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], _stateChangedObservable); + } + + public void Accept(StateMachineInspector inspector) + { + Initial.Accept(inspector); + + _stateCache.Each(x => + { + if (Equals(x, Initial) || Equals(x, Final)) + return; + + x.Accept(inspector); + }); + + Final.Accept(inspector); + } + + StateAccessor StateMachine.InstanceStateAccessor + { + get { return _instanceStateAccessor; } + } + + public State Initial { get; protected set; } + public State Final { get; private set; } + + State StateMachine.GetState(string name) + { + return _stateCache[name]; + } + + void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event) + { + composer.Execute(() => + { + State state = _instanceStateAccessor.Get(instance); + + State instanceState = _stateCache[state.Name]; + + return composer.ComposeEvent(instance, instanceState, @event); + }); + } + + void StateMachine.RaiseEvent(Composer composer, TInstance instance, Event @event, TData data) + { + composer.Execute(() => + { + State state = _instanceStateAccessor.Get(instance); + + State instanceState = _stateCache[state.Name]; + + return composer.ComposeEvent(instance, instanceState, @event, data); + }); + } + + public State GetState(string name) + { + return _stateCache[name]; + } + + public IEnumerable States + { + get { return _stateCache; } + } + + Event StateMachine.GetEvent(string name) + { + return _eventCache[name].Event; + } + + public IEnumerable Events + { + get { return _eventCache.Select(x => x.Event); } + } + + public Type InstanceType + { + get { return typeof(TInstance); } + } + + public IEnumerable NextEvents(State state) + { + return _stateCache[state.Name].Events; + } + + public IObservable> StateChanged + { + get { return _stateChangedObservable; } + } + + public IObservable> EventRaising(Event @event) + { + if (!_eventCache.Has(@event.Name)) + throw new ArgumentException("Unknown event: " + @event.Name, "event"); + + return _eventCache[@event.Name].EventRaising; + } + + public IObservable> EventRaised(Event @event) + { + if (!_eventCache.Has(@event.Name)) + throw new ArgumentException("Unknown event: " + @event.Name, "event"); + + return _eventCache[@event.Name].EventRaised; + } + + /// + /// Declares what property holds the TInstance's state on the current instance of the state machine + /// + /// + /// Setting the state accessor more than once will cause the property managed by the state machine to change each time. + /// Please note, the state machine can only manage one property at a given time per instance, + /// and the best practice is to manage one property per machine. + /// + protected void InstanceState(Expression> instanceStateProperty) + { + _instanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, + _stateCache[Initial.Name], _stateChangedObservable); + } + + void RegisterEvents() + { + var properties = GetProperties(GetType()) + .Where(x => typeof(Event).IsAssignableFrom(x.PropertyType)); // TODO: this may just need to be equals + + foreach (var property in properties) + { + var name = property.Name; + + Event @event; + + if (property.PropertyType.IsGenericType) + { + var dataEventType = OpenGenericDataEventType.MakeGenericType(property.PropertyType.GetGenericArguments()); + + @event = (Event)Activator.CreateInstance(dataEventType, name); + } + else + { + @event = new SimpleEvent(name); + } + + _eventCache[name] = new StateMachineEvent(@event); + + property.SetValue(this, @event); + } + } + + void RegisterStates() + { + var properties = GetProperties(GetType()) + .Where(x => typeof(State).IsAssignableFrom(x.PropertyType)); // TODO: this may just need to be equals + + foreach (var property in properties) + { + var name = property.Name; + + State state = new StateImpl(name, _eventRaisingObserver, _eventRaisedObserver); + + _stateCache[name] = state; + + property.SetValue(this, state); + } + } + + /// + /// Adds a composite event to the state machine. A composite event is triggered when all + /// off the required events have been raised. Note that required events cannot be in the initial + /// state since it would cause extra instances of the state machine to be created + /// + /// The composite event + /// The property in the instance used to track the state of the composite event + /// The events that must be raised before the composite event is raised + protected void ComposeEvent(Event @event, + Expression> trackingPropertyExpression, + params Event[] events) + { + // TODO: check @event for null??? + + if (events == null) + { + throw new ArgumentNullException("events"); + } + + if (events.Length > 31) + { + throw new ArgumentException("No more than 31 events can be combined into a single event"); + } + + if (events.Length == 0) + { + throw new ArgumentException("At least one event must be specified for a composite event"); + } + + if (events.Any(x => x == null)) + { + throw new ArgumentException("One or more events specified has not yet been initialized"); + } + + PropertyInfo trackingPropertyInfo = trackingPropertyExpression.GetPropertyInfo(); + + var complete = new CompositeEventStatus(Enumerable.Range(0, events.Length) + .Aggregate(0, (current, x) => current | (1 << x))); + + for (int i = 0; i < events.Length; i++) + { + int flag = 1 << i; + + var activity = new CompositeEventActivity(trackingPropertyInfo, flag, complete, + (consumer, instance) => ((StateMachine) this).RaiseEvent(consumer, instance, @event)); + + foreach (var state in _stateCache.Where(x => !Equals(x, Initial))) + { + During(state, + When(events[i]) + .Then(() => activity)); + } + } + } + + protected void During(State state, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state, eventActivities); + } + + protected void During(State state1, State state2, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state1, eventActivities); + BindActivitiesToState(state2, eventActivities); + } + + protected void During(State state1, State state2, State state3, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state1, eventActivities); + BindActivitiesToState(state2, eventActivities); + BindActivitiesToState(state3, eventActivities); + } + + protected void During(State state1, State state2, State state3, State state4, + params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + BindActivitiesToState(state1, eventActivities); + BindActivitiesToState(state2, eventActivities); + BindActivitiesToState(state3, eventActivities); + BindActivitiesToState(state4, eventActivities); + } + + protected void During(IEnumerable states, params IEnumerable>[] activities) + { + EventActivity[] eventActivities = activities.SelectMany(x => x).ToArray(); + + foreach (var state in states) + { + BindActivitiesToState(state, eventActivities); + } + } + + static void BindActivitiesToState(State state, IEnumerable> eventActivities) + { + State activityState = state.For(); + + foreach (var activity in eventActivities) + activityState.Bind(activity); + } + + protected void Initially(params IEnumerable>[] activities) + { + During(Initial, activities); + } + + protected void DuringAny(params IEnumerable>[] activities) + { + IEnumerable> states = _stateCache.Where(x => !Equals(x, Initial) && !Equals(x, Final)); + + // we only add DuringAny event handlers to non-initial and non-final states to avoid + // reviving finalized state machine instances or creating new ones accidentally. + foreach (var state in states) + During(state, activities); + + BindTransitionEvents(Initial, activities); + BindTransitionEvents(Final, activities); + } + + void BindTransitionEvents(State state, IEnumerable>> activities) + { + IEnumerable> eventActivities = activities + .SelectMany(activity => activity.Where(x => IsTransitionEvent(state, x.Event))); + + foreach (var eventActivity in eventActivities) + During(state, new[] { eventActivity }); + } + + bool IsTransitionEvent(State state, Event eevent) + { + return Equals(eevent, state.Enter) || Equals(eevent, state.BeforeEnter) + || Equals(eevent, state.AfterLeave) || Equals(eevent, state.Leave); + } + + protected void Finally(Func, EventActivityBinder> activityCallback) + { + EventActivityBinder binder = When(Final.Enter); + + binder = activityCallback(binder); + + DuringAny(binder); + } + + protected EventActivityBinder When(Event @event) + { + return new SimpleEventActivityBinder(this, @event); + } + + protected EventActivityBinder When(Event @event) + { + return new DataEventActivityBinder(this, @event); + } + + protected EventActivityBinder When(Event @event, + Expression> filterExpression) + { + return new DataEventActivityBinder(this, @event, filterExpression); + } + } } From d143fc68fa7b97a83267204cc349cbc813c39f2a Mon Sep 17 00:00:00 2001 From: Chris Block Date: Sat, 18 Oct 2014 16:46:48 -0500 Subject: [PATCH 4/7] Fix accidentally changed setter accessibility. --- src/Automatonymous/AutomatonymousStateMachine.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Automatonymous/AutomatonymousStateMachine.cs b/src/Automatonymous/AutomatonymousStateMachine.cs index fe7afcb..6b2f3e7 100644 --- a/src/Automatonymous/AutomatonymousStateMachine.cs +++ b/src/Automatonymous/AutomatonymousStateMachine.cs @@ -132,7 +132,7 @@ StateAccessor StateMachine.InstanceStateAccessor get { return _instanceStateAccessor; } } - public State Initial { get; protected set; } + public State Initial { get; private set; } public State Final { get; private set; } State StateMachine.GetState(string name) From c8afd8adeb9c2763fe860760cd5dcce3e2f5cc00 Mon Sep 17 00:00:00 2001 From: Chris Block Date: Wed, 22 Oct 2014 00:57:24 -0500 Subject: [PATCH 5/7] Split tests out to have one assert each. --- .../AutomatonymousStateMachine_Specs.cs | 75 ++++++++++++++++--- 1 file changed, 64 insertions(+), 11 deletions(-) diff --git a/src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs b/src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs index e37a306..3db6264 100644 --- a/src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs +++ b/src/Automatonymous.Tests/AutomatonymousStateMachine_Specs.cs @@ -27,22 +27,55 @@ public void Should_register_all_state_properties() var states = stateMachine.States.ToList(); - Assert.That(states, Has.Count.EqualTo(4)); - Assert.That(states, Contains.Item(stateMachine.Initial)); - Assert.That(states, Contains.Item(stateMachine.Final)); - Assert.That(states, Contains.Item(stateMachine.ThisIsAState)); - Assert.That(states, Contains.Item(stateMachine.ThisIsAlsoAState)); + Assert.That(states, Has.Count.EqualTo(3)); + } + + [Test] + public void Should_register_inherited_initial_state_property() + { + var stateMachine = new TestStateMachine(); + + Assert.That(stateMachine.States, Contains.Item(stateMachine.Initial)); + } + + [Test] + public void Should_register_inherited_final_state_property() + { + var stateMachine = new TestStateMachine(); + + Assert.That(stateMachine.States, Contains.Item(stateMachine.Final)); + } + + [Test] + public void Should_register_declared_state_property() + { + var stateMachine = new TestStateMachine(); + + Assert.That(stateMachine.States, Contains.Item(stateMachine.ThisIsAState)); } [Test] - public void Should_initialize_all_state_properties() + public void Should_initialize_inherited_initial_state_property() { var stateMachine = new TestStateMachine(); Assert.That(stateMachine.Initial, Is.Not.Null); + } + + [Test] + public void Should_initialize_inherited_final_state_property() + { + var stateMachine = new TestStateMachine(); + Assert.That(stateMachine.Final, Is.Not.Null); + } + + [Test] + public void Should_initialize_declared_state_property() + { + var stateMachine = new TestStateMachine(); + Assert.That(stateMachine.ThisIsAState, Is.Not.Null); - Assert.That(stateMachine.ThisIsAlsoAState, Is.Not.Null); } [Test] @@ -53,16 +86,37 @@ public void Should_register_all_event_properties() var events = stateMachine.Events.ToList(); Assert.That(events, Has.Count.EqualTo(2)); - Assert.That(events, Contains.Item(stateMachine.ThisIsASimpleEvent)); - Assert.That(events, Contains.Item(stateMachine.ThisIsAnEventConsumingData)); } [Test] - public void Should_initialize_all_event_properties() + public void Should_register_simple_event_property() + { + var stateMachine = new TestStateMachine(); + + Assert.That(stateMachine.Events, Contains.Item(stateMachine.ThisIsASimpleEvent)); + } + + [Test] + public void Should_register_generic_event_property() + { + var stateMachine = new TestStateMachine(); + + Assert.That(stateMachine.Events, Contains.Item(stateMachine.ThisIsAnEventConsumingData)); + } + + [Test] + public void Should_initialize_simple_event_property() { var stateMachine = new TestStateMachine(); Assert.That(stateMachine.ThisIsASimpleEvent, Is.Not.Null); + } + + [Test] + public void Should_initialize_generic_event_property() + { + var stateMachine = new TestStateMachine(); + Assert.That(stateMachine.ThisIsAnEventConsumingData, Is.Not.Null); } @@ -80,7 +134,6 @@ class EventData class TestStateMachine : AutomatonymousStateMachine { public State ThisIsAState { get; private set; } - public State ThisIsAlsoAState { get; private set; } public Event ThisIsASimpleEvent { get; private set; } public Event ThisIsAnEventConsumingData { get; private set; } } From dea4968b33dcbadc7030a6a6308d996fcda4734b Mon Sep 17 00:00:00 2001 From: Chris Block Date: Wed, 22 Oct 2014 00:59:48 -0500 Subject: [PATCH 6/7] Minor optimizations. --- .../AutomatonymousStateMachine.cs | 37 ++++++------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/Automatonymous/AutomatonymousStateMachine.cs b/src/Automatonymous/AutomatonymousStateMachine.cs index 6b2f3e7..03c82a8 100644 --- a/src/Automatonymous/AutomatonymousStateMachine.cs +++ b/src/Automatonymous/AutomatonymousStateMachine.cs @@ -40,31 +40,19 @@ public abstract class AutomatonymousStateMachine protected static readonly Type OpenGenericDataEventType = typeof (DataEvent<>); // TODO: make this an Extension Method of Type - private static IEnumerable GetInheritanceChain(Type type) + private static IEnumerable GetInheritanceChain(Type type, bool includeSelf = true) { - var result = new List - { - type - }; - - var b = type.BaseType; + var b = includeSelf ? type : type.BaseType; while (b != null) { - result.Add(b); + yield return b; b = b.BaseType; } - - return result; } protected static IEnumerable GetProperties(Type type) - { - return GetProperties(type, BindingFlags.Public | BindingFlags.Instance); - } - - protected static IEnumerable GetProperties(Type type, BindingFlags flags) { if (Properties.ContainsKey(type) == false) { @@ -73,7 +61,8 @@ protected static IEnumerable GetProperties(Type type, BindingFlags if (Properties.ContainsKey(type) == false) { var properties = GetInheritanceChain(type) - .SelectMany(x => x.GetProperties(flags | BindingFlags.DeclaredOnly)); + .SelectMany(x => x.GetProperties(BindingFlags.Public | BindingFlags.Instance | BindingFlags.DeclaredOnly)) + .ToList(); Properties[type] = properties; } @@ -95,9 +84,8 @@ public abstract class AutomatonymousStateMachine : readonly EventRaisingObserver _eventRaisingObserver; readonly Cache> _stateCache; readonly Observable> _stateChangedObservable; - StateAccessor _instanceStateAccessor; - protected AutomatonymousStateMachine() + protected AutomatonymousStateMachine() { _stateCache = new DictionaryCache>(); _eventCache = new DictionaryCache>(); @@ -109,7 +97,7 @@ protected AutomatonymousStateMachine() RegisterStates(); RegisterEvents(); - _instanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], _stateChangedObservable); + InstanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], _stateChangedObservable); } public void Accept(StateMachineInspector inspector) @@ -127,10 +115,7 @@ public void Accept(StateMachineInspector inspector) Final.Accept(inspector); } - StateAccessor StateMachine.InstanceStateAccessor - { - get { return _instanceStateAccessor; } - } + public StateAccessor InstanceStateAccessor { get; private set; } public State Initial { get; private set; } public State Final { get; private set; } @@ -144,7 +129,7 @@ void StateMachine.RaiseEvent(Composer composer, TInstance instance, E { composer.Execute(() => { - State state = _instanceStateAccessor.Get(instance); + State state = InstanceStateAccessor.Get(instance); State instanceState = _stateCache[state.Name]; @@ -156,7 +141,7 @@ void StateMachine.RaiseEvent(Composer composer, TInstance inst { composer.Execute(() => { - State state = _instanceStateAccessor.Get(instance); + State state = InstanceStateAccessor.Get(instance); State instanceState = _stateCache[state.Name]; @@ -225,7 +210,7 @@ public IObservable> EventRaised(Event @event) /// protected void InstanceState(Expression> instanceStateProperty) { - _instanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, + InstanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, _stateCache[Initial.Name], _stateChangedObservable); } From d1a302a6075b832ecf3c985d341516e36e364ea0 Mon Sep 17 00:00:00 2001 From: Chris Block Date: Wed, 22 Oct 2014 01:10:53 -0500 Subject: [PATCH 7/7] Revert some of ReSharpers overzealous changes. --- src/Automatonymous/AutomatonymousStateMachine.cs | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/Automatonymous/AutomatonymousStateMachine.cs b/src/Automatonymous/AutomatonymousStateMachine.cs index 03c82a8..0a1d7ef 100644 --- a/src/Automatonymous/AutomatonymousStateMachine.cs +++ b/src/Automatonymous/AutomatonymousStateMachine.cs @@ -84,8 +84,9 @@ public abstract class AutomatonymousStateMachine : readonly EventRaisingObserver _eventRaisingObserver; readonly Cache> _stateCache; readonly Observable> _stateChangedObservable; + StateAccessor _instanceStateAccessor; - protected AutomatonymousStateMachine() + protected AutomatonymousStateMachine() { _stateCache = new DictionaryCache>(); _eventCache = new DictionaryCache>(); @@ -97,7 +98,7 @@ protected AutomatonymousStateMachine() RegisterStates(); RegisterEvents(); - InstanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], _stateChangedObservable); + _instanceStateAccessor = new DefaultInstanceStateAccessor(_stateCache[Initial.Name], _stateChangedObservable); } public void Accept(StateMachineInspector inspector) @@ -115,7 +116,10 @@ public void Accept(StateMachineInspector inspector) Final.Accept(inspector); } - public StateAccessor InstanceStateAccessor { get; private set; } + public StateAccessor InstanceStateAccessor + { + get { return _instanceStateAccessor; } + } public State Initial { get; private set; } public State Final { get; private set; } @@ -129,7 +133,7 @@ void StateMachine.RaiseEvent(Composer composer, TInstance instance, E { composer.Execute(() => { - State state = InstanceStateAccessor.Get(instance); + State state = _instanceStateAccessor.Get(instance); State instanceState = _stateCache[state.Name]; @@ -141,7 +145,7 @@ void StateMachine.RaiseEvent(Composer composer, TInstance inst { composer.Execute(() => { - State state = InstanceStateAccessor.Get(instance); + State state = _instanceStateAccessor.Get(instance); State instanceState = _stateCache[state.Name]; @@ -210,7 +214,7 @@ public IObservable> EventRaised(Event @event) /// protected void InstanceState(Expression> instanceStateProperty) { - InstanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, + _instanceStateAccessor = new InitialIfNullStateAccessor(instanceStateProperty, _stateCache[Initial.Name], _stateChangedObservable); }