README.md in action_logic-0.2.0 vs README.md in action_logic-0.2.1

- old
+ new

@@ -160,13 +160,13 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_before :expected_attribute1 => { :type => :string }, - :expected_attribute2 => { :type => :integer, :presence => true } - validates_after :example_attribute1 => { :type => :string, :presence => ->(example_attribute1) { !example_attribute1.empty? } } + validates_before :expected_attribute1 => { :type => String }, + :expected_attribute2 => { :type => Fixnum, :presence => true } + validates_after :example_attribute1 => { :type => String, :presence => ->(example_attribute1) { !example_attribute1.empty? } } def call # adds `example_attribute1` to the shared `context` with the value "Example value" context.example_attribute1 = "New value from context attributes: #{context.expected_attribute1} #{context.expected_attribute2}" end @@ -177,34 +177,41 @@ # The result object is the shared context object (an instance of ActionContext): result # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_attribute1="New value from context attributes: example 123"> ``` -The `ActionTaskExample` is invoked using the static method `execute` which takes an optional hash of attributes that is converted into an `ActionContext`. Assuming the before validations are satisfied, the `call` method is invoked. In the body of the `call` method the `ActionTask` can access the shared `ActionContext` instance via a `context` object. This shared `context` object allows for getting and setting attributes as needed. When the `call` method returns, the `context` is validated against any defined after validations, and the `context` is then returned to the caller. +The `ActionTaskExample` is invoked using the static method `execute` which takes an optional hash of attributes that is converted into an `ActionContext`. +Assuming the before validations are satisfied, the `call` method is invoked. In the body of the `call` method the `ActionTask` can access the shared `ActionContext` +instance via a `context` object. This shared `context` object allows for getting and setting attributes as needed. When the `call` method returns, the `context` +is validated against any defined after validations, and the `context` is then returned to the caller. The diagram below is a visual representation of how an `ActionTask` is evaluted when its `execute` method is invoked from a caller: <img src="https://raw.githubusercontent.com/rewinfrey/action_logic/master/resources/action_task_diagram.png" /> -Although this example is for the `ActionTask` abstraction, `ActionUseCase` and `ActionCoordinator` follow the same pattern. The difference is that `ActionUseCase` is designed to organize multiple `ActionTasks`, and `ActionCoordinator` is designed to organize many `ActionUseCases`. +Although this example is for the `ActionTask` abstraction, `ActionUseCase` and `ActionCoordinator` follow the same pattern. The difference is that `ActionUseCase` +is designed to organize multiple `ActionTasks`, and `ActionCoordinator` is designed to organize many `ActionUseCases`. ### ActionUseCase -As business logic grows in complexity the number of steps or tasks required to fulfill that business logic tends to increase. Managing this complexity is a problem every team must face. Abstractions can help teams of varying experience levels work together and promote code that remains modular and simple to understand and extend. `ActionUseCase` represents a layer of abstraction that organizes multiple `ActionTasks` and executes each `ActionTask` in the order they are defined. Each task receives the same shared `context` so tasks can be composed together. +As business logic grows in complexity the number of steps or tasks required to fulfill that business logic tends to increase. Managing this complexity is a problem every team must face. +Abstractions can help teams of varying experience levels work together and promote code that remains modular and simple to understand and extend. `ActionUseCase` represents a layer of +abstraction that organizes multiple `ActionTasks` and executes each `ActionTask` in the order they are defined. Each task receives the same shared `context` so tasks can be composed together. -To implement an `ActionUseCase` class you must define a `call` method and a `tasks` method. You also can specify any before, after or around validations or an error handler. The following is an example showcasing how an `ActionUseCase` class organizes the execution of multiple `ActionTasks` and defines before and after validations on the shared `context`: +To implement an `ActionUseCase` class you must define a `call` method and a `tasks` method. You also can specify any before, after or around validations or an error handler. +The following is an example showcasing how an `ActionUseCase` class organizes the execution of multiple `ActionTasks` and defines before and after validations on the shared `context`: ```ruby class ActionUseCaseExample include ActionLogic::ActionUseCase - validates_before :expected_attribute1 => { :type => :string }, - :expected_attribute2 => { :type => :integer, :presence => true } - validates_after :example_task1 => { :type => :boolean, :presence => true }, - :example_task2 => { :type => :boolean, :presence => true }, - :example_task3 => { :type => :boolean, :presence => true }, - :example_usecase1 => { :type => :boolean, :presence => true } + validates_before :expected_attribute1 => { :type => String }, + :expected_attribute2 => { :type => Fixnum, :presence => true } + validates_after :example_task1 => { :type => TrueClass, :presence => true }, + :example_task2 => { :type => TrueClass, :presence => true }, + :example_task3 => { :type => TrueClass, :presence => true }, + :example_usecase1 => { :type => TrueClass, :presence => true } # The `call` method is invoked prior to invoking any of the ActionTasks defined by the `tasks` method. # The purpose of the `call` method allows us to prepare the shared `context` prior to invoking the ActionTasks. def call context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success> @@ -218,31 +225,31 @@ end end class ActionTaskExample1 include ActionLogic::ActionTask - validates_after :example_task1 => { :type => :boolean, :presence => true } + validates_after :example_task1 => { :type => TrueClass, :presence => true } def call context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true> context.example_task1 = true end end class ActionTaskExample2 include ActionLogic::ActionTask - validates_after :example_task2 => { :type => :boolean, :presence => true } + validates_after :example_task2 => { :type => TrueClass, :presence => true } def call context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true, example_task1=true> context.example_task2 = true end end class ActionTaskExample3 include ActionLogic::ActionTask - validates_after :example_task3 => { :type => :boolean, :presence => true } + validates_after :example_task3 => { :type => TrueClass, :presence => true } def call context # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true, example_task1=true, example_task2=true> context.example_task3 = true end @@ -252,21 +259,28 @@ result = ActionUseCaseExample.execute(:expected_attribute1 => "example", :expected_attribute2 => 123) result # => #<ActionLogic::ActionContext expected_attribute1="example", expected_attribute2=123, status=:success, example_usecase1=true, example_task1=true, example_task2=true, example_task3=true> ``` -By following the value of the shared `context` from the `ActionUseCaseExample` to each of the `ActionTask` classes, it is possible to see how the shared `context` is mutated to accomodate the various attributes and their values each execution context adds to the `context`. It also reveals the order in which the `ActionTasks` are evaluated, and indicates that the `call` method of the `ActionUseCaseExample` is invoked prior to any of the `ActionTasks` defined in the `tasks` method. +By following the value of the shared `context` from the `ActionUseCaseExample` to each of the `ActionTask` classes, it is possible to see how the shared `context` +is mutated to accomodate the various attributes and their values each execution context adds to the `context`. It also reveals the order in which the `ActionTasks` +are evaluated, and indicates that the `call` method of the `ActionUseCaseExample` is invoked prior to any of the `ActionTasks` defined in the `tasks` method. -To help visualize the flow of execution when an `ActionUseCase` is invoked, this diagram aims to illustrate the relationship between `ActionUseCase` and `ActionTasks` and the order in which operations are performed: +To help visualize the flow of execution when an `ActionUseCase` is invoked, this diagram aims to illustrate the relationship between `ActionUseCase` and `ActionTasks` +and the order in which operations are performed: <img src="https://raw.githubusercontent.com/rewinfrey/action_logic/master/resources/action_use_case_diagram.png" /> ### ActionCoordinator -Sometimes the behavior we wish our Ruby or Rails application to provide requires us to coordinate work between various domains of our application's business logic. The `ActionCoordinator` abstraction is intended to help coordinate multiple `ActionUseCases` by allowing you to define a plan of which `ActionUseCases` to invoke depending on the outcome of each `ActionUseCase` execution. The `ActionCoordinator` abstraction is the highest level of abstraction in `ActionLogic`. +Sometimes the behavior we wish our Ruby or Rails application to provide requires us to coordinate work between various domains of our application's business logic. +The `ActionCoordinator` abstraction is intended to help coordinate multiple `ActionUseCases` by allowing you to define a plan of which `ActionUseCases` to invoke +depending on the outcome of each `ActionUseCase` execution. The `ActionCoordinator` abstraction is the highest level of abstraction in `ActionLogic`. -To implement an `ActionCoordinator` class, you must define a `call` method in addition to a `plan` method. The purpose of the `plan` method is to define a state transition map that links together the various `ActionUseCase` classes the `ActionCoordinator` is organizing, as well as allowing you to define error or halt scenarios based on the result of each `ActionUseCase`. The following code example demonstrates a simple `ActionCoordinator`: +To implement an `ActionCoordinator` class, you must define a `call` method in addition to a `plan` method. The purpose of the `plan` method is to define a state +transition map that links together the various `ActionUseCase` classes the `ActionCoordinator` is organizing, as well as allowing you to define error or halt +scenarios based on the result of each `ActionUseCase`. The following code example demonstrates a simple `ActionCoordinator`: ```ruby class ActionCoordinatorExample include ActionLogic::ActionCoordinator @@ -287,11 +301,11 @@ end class ActionUseCaseExample1 include ActionLogic::ActionUseCase - validates_before :required_attribute1 => { :type => :string } + validates_before :required_attribute1 => { :type => String } def call context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2"> context.example_usecase1 = true end @@ -303,11 +317,11 @@ end class ActionUseCaseExample2 include ActionLogic::ActionUseCase - validates_before :required_attribute2 => { :type => :string } + validates_before :required_attribute2 => { :type => String } def call context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2", example_usecase1=true, example_task1=true> context.example_usecase2 = true end @@ -332,21 +346,21 @@ end end class ActionTaskExample1 include ActionLogic::ActionTask - validates_after :example_task1 => { :type => :boolean, :presence => true } + validates_after :example_task1 => { :type => TrueClass, :presence => true } def call context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2", example_usecase1=true> context.example_task1 = true end end class ActionTaskExample2 include ActionLogic::ActionTask - validates_after :example_task2 => { :type => :boolean, :presence => true } + validates_after :example_task2 => { :type => TrueClass, :presence => true } def call context # => #<ActionLogic::ActionContext status=:success, required_attribute1="required attribute 1", required_attribute2="required attribute 2", example_usecase1=true, example_task1=true, example_usecase2=true> context.example_task2 = true end @@ -358,13 +372,13 @@ ``` <img src="https://raw.githubusercontent.com/rewinfrey/action_logic/master/resources/action_coordinator_diagram.png" /> ### Succeeding an `ActionContext` -By default, the value of the `status` attribute of instances of `ActionContext` is `:success`. Normally this is useful information for the caller of an `ActionTask`, `ActionUseCase` or `ActionCoordinator` -because it informs the caller that the various execution context(s) were successful. In other words, a `:success` status indicates that none of the execution contexts had a failure -or halted execution. +By default, the value of the `status` attribute of instances of `ActionContext` is `:success`. Normally this is useful information for the caller of an `ActionTask`, +`ActionUseCase` or `ActionCoordinator` because it informs the caller that the various execution context(s) were successful. In other words, a `:success` status +indicates that none of the execution contexts had a failure or halted execution. ### Failing an `ActionContext` Using `context.fail!` does two important things: it immediately stops the execution of any proceeding business logic (prevents any additional `ActionTasks` from executing) and also sets the status of the `context` as `:failure`. This status is most applicable to the caller or an `ActionCoordinator` that might have a plan specifically for a `:failure` status of a resulting `ActionUseCase`. @@ -587,24 +601,26 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_after :integer_test => { :type => :integer }, - :float_test => { :type => :float }, - :string_test => { :type => :string }, - :bool_test => { :type => :boolean }, - :hash_test => { :type => :hash }, - :array_test => { :type => :array }, - :symbol_test => { :type => :symbol }, - :nil_test => { :type => :nil } + validates_after :integer_test => { :type => Fixnum }, + :float_test => { :type => Float }, + :string_test => { :type => String }, + :truthy_test => { :type => TrueClass }, + :falsey_test => { :type => FalseClass }, + :hash_test => { :type => Hash }, + :array_test => { :type => Array }, + :symbol_test => { :type => Symbol }, + :nil_test => { :type => NilClass } def call context.integer_test = 123 context.float_test = 1.0 context.string_test = "test" - context.bool_test = true + context.truthy_test = true + context.falsey_test = false context.hash_test = {} context.array_test = [] context.symbol_test = :symbol context.nil_test = nil end @@ -614,35 +630,36 @@ result # => #<ActionLogic::ActionContext status=:success, # integer_test=123, # float_test=1.0, # string_test="test", - # bool_test=true, + # truthy_test=true, + # falsey_test=false, # hash_test={}, # array_test=[], # symbol_test=:symbol, # nil_test=nil> ``` It's important to point out that Ruby's `true` and `false` are not `Boolean` but `TrueClass` and `FalseClass` respectively. Additionally, `nil`'s type is `NilClass` in Ruby. -To simplify the way these validations work for `true` or `false`, type validations expect the symbol `:boolean` as the `:type`. `nil` is validated simply with the `:nil` `:type`. +Also potentially surprising to some is that Ruby's integer type is of class `Fixnum`, but floats are of class `Float`. As we saw with attribute validations, if an attribute's value does not conform to the type expected, `ActionLogic` will raise an `ActionLogic::AttributeTypeError` with a detailed description about which attribute's value failed the validation: ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_after :integer_test => { :type => :integer } + validates_after :integer_test => { :type => Fixnum } def call context.integer_test = 1.0 end end -ActionTaskExample.execute # ~> ["Attribute: integer_test with value: 1.0 was expected to be of type integer but is float"] (ActionLogic::AttributeTypeError) +ActionTaskExample.execute # ~> ["Attribute: integer_test with value: 1.0 was expected to be of type Fixnum but is Float"] (ActionLogic::AttributeTypeError) ``` In addition to the above default types it is possible to also validate against user defined types. ### Custom Type Validations @@ -655,11 +672,11 @@ end class ActionTaskExample include ActionLogic::ActionTask - validates_after :example_attribute => { :type => :exampleclass } + validates_after :example_attribute => { :type => ExampleClass } def call context.example_attribute = ExampleClass.new end end @@ -668,12 +685,11 @@ result # => #<ActionLogic::ActionContext status=:success, example_attribute=#<ExampleClass:0x007f95d1922bd8>> ``` In the above example, a custom class `ExampleClass` is defined. In order to type validate against this class, the required format for the name of the class is simply -the lowercase version of the class as a symbol. `ExampleClass` becomes `:exampleclass`, `UserAttributes` becomes `:userattributes`, -`ReallyLongClassNameThatBreaks80ColumnsInVimRule` becomes `:reallylongclassnamethatbreaks80columnsinvimrule` and so on. +the class constant `ExampleClass`. If a custom type validation fails, `ActionLogic` provides the same `ActionLogic::AttributeTypeError` with a detailed explanation about what attribute is in violation of the type validation: ```ruby @@ -684,18 +700,18 @@ end class ActionTaskExample include ActionLogic::ActionTask - validates_after :example_attribute => { :type => :exampleclass } + validates_after :example_attribute => { :type => ExampleClass } def call context.example_attribute = OtherClass.new end end -ActionTaskExample.execute # ~> ["Attribute: example_attribute with value: #<OtherClass:0x007fb5ca04edb8> was expected to be of type exampleclass but is otherclass"] (ActionLogic::AttributeTypeError) +ActionTaskExample.execute # ~> ["Attribute: example_attribute with value: #<OtherClass:0x007fb5ca04edb8> was expected to be of type ExampleClass but is OtherClass"] (ActionLogic::AttributeTypeError) ``` Attribute and type validations are very helpful, but in some situations this is not enough. Additionally, `ActionLogic` provides presence validation so you can also verify that a given attribute on a context not only has the correct type, but also has a value that is considered `present`. @@ -794,11 +810,11 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_before :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } } + validates_before :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } } def call end end @@ -811,12 +827,12 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_before :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } }, - :example_attribute2 => { :type => :integer } + validates_before :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } }, + :example_attribute2 => { :type => Fixnum } def call end end @@ -836,11 +852,11 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_after :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } } + validates_after :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } } def call context.example_attribute = [1, 2, 3] end end @@ -853,12 +869,12 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_after :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } }, - :example_attribute2 => { :type => :integer } + validates_after :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } }, + :example_attribute2 => { :type => Fixnum } def call context.example_attribute = [1, 2, 3] context.example_attribute2 = 1 end @@ -880,11 +896,11 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_around :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } } + validates_around :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } } def call end end @@ -896,11 +912,11 @@ ```ruby class ActionTaskExample include ActionLogic::ActionTask - validates_around :example_attribute => { :type => :array, :presence => ->(attribute) { attribute.any? } }, - :example_attribute2 => { :type => :integer } + validates_around :example_attribute => { :type => Array, :presence => ->(attribute) { attribute.any? } }, + :example_attribute2 => { :type => Fixnum } def call end end