active_operation 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: af093d7762abf6f9113d7886eef00e829cef071a
4
- data.tar.gz: 07b3bf0cde1c490de17b16c3d93fc16e9d0bd14a
3
+ metadata.gz: eead73f1ad502fdd329a6cd5ae6acc20306472df
4
+ data.tar.gz: c4adb13fbb34e04cf4895175babc9d458ff8c6bc
5
5
  SHA512:
6
- metadata.gz: c8508ccc57a478db664e57e576bbaaa0be53d32ed625314a324b8559d49fa98df406759bb5ee3914b3d7f43ca958e43520f41da265a91021f2490fdea89e981c
7
- data.tar.gz: 73c32d9434395e5e3d6d4b667b6c2fe6e621efb10e31cfdea5559380efedd0f901e3d422d4109d7197dc53c928b424a402d37f22a1ec6a5962dde0d08d34b8f6
6
+ metadata.gz: 32c999e6bd87a939c985ea696250cf00de302fe0b7749edb07339381e1a543c4baaa54bc10b3101b0215f34f9955e9bf13d555ac581a693d276f029015ad0d82
7
+ data.tar.gz: ac35417f23a2a4321e11869b3c95f8a0120da18cf4b71cbae7ad22494d3eb04b8638f2d119cdaa960cb25c84de908bc63a9a3fb4b9eba736e0b385ecd8afb6fe
data/README.md CHANGED
@@ -26,6 +26,20 @@ Or install it yourself as:
26
26
  $ gem install active_operation
27
27
  ```
28
28
 
29
+ ### Rails
30
+
31
+ We recommend running the install generator to initialize a base operation:
32
+
33
+ ```
34
+ rails g active_operation:install
35
+ ```
36
+
37
+ You can also generate new operations using:
38
+
39
+ ```
40
+ rails g active_operation:operation Signup
41
+ ```
42
+
29
43
  ## Usage
30
44
 
31
45
  To define an operation, create a new class and inherit from `ActiveOperation::Base`.
@@ -1,4 +1,5 @@
1
1
  require "active_support/callbacks"
2
+ require "delegate"
2
3
  require "smart_properties"
3
4
 
4
5
  module ActiveOperation
@@ -6,6 +7,9 @@ module ActiveOperation
6
7
  class AlreadyCompletedError < Error; end
7
8
  end
8
9
 
9
- require "active_operation/version"
10
- require "active_operation/input"
11
- require "active_operation/base"
10
+ require_relative "active_operation/version"
11
+ require_relative "active_operation/input"
12
+ require_relative "active_operation/base"
13
+ require_relative "active_operation/pipeline"
14
+
15
+ require_relative "active_operation/matcher" if defined?(::RSpec)
@@ -1,3 +1,5 @@
1
+ require 'active_support/callbacks'
2
+
1
3
  class ActiveOperation::Base
2
4
  include SmartProperties
3
5
  include ActiveSupport::Callbacks
@@ -14,14 +16,24 @@ class ActiveOperation::Base
14
16
  define_callbacks :halted, scope: [:name]
15
17
 
16
18
  class << self
17
- def call(*args)
19
+ def perform(*args)
18
20
  new(*args).call
19
21
  end
20
22
 
23
+ def call(*args)
24
+ perform(*args)
25
+ end
26
+
21
27
  def inputs
22
28
  []
23
29
  end
24
30
 
31
+ def to_proc
32
+ ->(*args) {
33
+ perform(*args)
34
+ }
35
+ end
36
+
25
37
  protected
26
38
 
27
39
  def input(name, type: :positional, **configuration)
@@ -70,6 +82,8 @@ class ActiveOperation::Base
70
82
  end
71
83
 
72
84
  def inherited(subclass)
85
+ super
86
+
73
87
  subclass.define_singleton_method(:inputs) do
74
88
  superclass.inputs + Array(@inputs)
75
89
  end
@@ -82,19 +96,21 @@ class ActiveOperation::Base
82
96
  end
83
97
  end
84
98
 
85
- def initialize(*positional_arguments, **keyword_arguments)
86
- expected_positional_arguments = self.class.inputs.select(&:positional?)
99
+ def initialize(*args)
100
+ arity = self.class.inputs.count(&:positional?)
101
+ arguments = args.shift(arity)
102
+ attributes = args.last.kind_of?(Hash) ? args.pop : {}
103
+
104
+ raise ArgumentError, "wrong number of arguments #{arguments.length + args.length} for #{arity}" unless args.empty?
87
105
 
88
- raise ArgumentError, "wrong number of arguments" if positional_arguments.length != expected_positional_arguments.length
106
+ self.class.inputs.select(&:positional?).each_with_index do |input, index|
107
+ attributes[input.name] = arguments[index]
108
+ end
89
109
 
90
- super(
91
- keyword_arguments.merge(
92
- expected_positional_arguments.zip(positional_arguments).map { |input, value| [input.name, value] }.to_h
93
- )
94
- )
110
+ super(attributes)
95
111
  end
96
112
 
97
- def call
113
+ def perform
98
114
  run_callbacks :execute do
99
115
  catch(:abort) do
100
116
  next if completed?
@@ -114,6 +130,10 @@ class ActiveOperation::Base
114
130
  raise
115
131
  end
116
132
 
133
+ def call
134
+ perform
135
+ end
136
+
117
137
  def output
118
138
  call unless self.completed?
119
139
  @output
@@ -0,0 +1,3 @@
1
+ require_relative 'matcher/execution'
2
+ require_relative 'matcher/utilize_operation'
3
+
@@ -0,0 +1,166 @@
1
+ module ActiveOperation
2
+ module Matcher
3
+ module Execution
4
+ class Base
5
+ def and_return(result)
6
+ @result = result
7
+ self
8
+ end
9
+
10
+ def when_initialized_with(*input)
11
+ @input = input
12
+ self
13
+ end
14
+
15
+ def failure_message
16
+ raise NotImplementedError, "Expected #{self.class} to implement ##{__callee__}"
17
+ end
18
+
19
+ def failure_message_when_negated
20
+ raise NotImplementedError, "Expected #{self.class} to implement ##{__callee__}"
21
+ end
22
+ alias negative_failure_message failure_message_when_negated
23
+
24
+ protected
25
+
26
+ attr_reader :operation
27
+ attr_reader :message
28
+ attr_reader :result
29
+ attr_reader :input
30
+
31
+ def operation=(operation)
32
+ operation = operation.new(*input) if operation.kind_of?(Class)
33
+ operation.perform
34
+ @operation = operation
35
+ end
36
+
37
+ def succeeded?
38
+ operation.succeeded?
39
+ end
40
+
41
+ def halted?
42
+ operation.halted?
43
+ end
44
+
45
+ def result_as_expected?
46
+ return true unless result
47
+ operation.output == result
48
+ end
49
+
50
+ def message_as_expected?
51
+ return true unless message
52
+ operation.message == message
53
+ end
54
+
55
+ def input_as_text
56
+ humanize(*input)
57
+ end
58
+
59
+ def result_as_text
60
+ humanize(result)
61
+ end
62
+
63
+ private
64
+
65
+ def humanize(*args)
66
+ args = args.map(&:inspect)
67
+ last_element = args.pop
68
+ args.length > 0 ? [args.join(", "), last_element].join(" and ") : last_element
69
+ end
70
+ end
71
+
72
+ class SucceedToPerform < Base
73
+ def matches?(operation)
74
+ self.operation = operation
75
+ succeeded? && result_as_expected?
76
+ end
77
+
78
+ def description
79
+ description = "succeed to perform"
80
+ description += " when initialized with custom input (#{input_as_text})" if input
81
+ description += " and return the expected result (#{result_as_text})" if result
82
+ description
83
+ end
84
+
85
+ def failure_message
86
+ "the operation failed to perform for the following reason(s):\n#{failure_reasons}"
87
+ end
88
+
89
+ def failure_message_when_negated
90
+ "the operation succeeded unexpectedly"
91
+ end
92
+
93
+ private
94
+
95
+ def failure_reasons
96
+ reasons = []
97
+ reasons << "it did not succeed at all" unless succeeded?
98
+ unless result_as_expected?
99
+ reasons << [
100
+ "it did not return the expected result",
101
+ "Expected: #{result.inspect}",
102
+ "Got: #{operation.result.inspect}"
103
+ ].join("\n\t ")
104
+ end
105
+ reasons.map { |r| "\t- #{r}" }.join("\n")
106
+ end
107
+ end
108
+
109
+ class HaltWhilePerforming < Base
110
+ def matches?(operation)
111
+ self.operation = operation
112
+ halted? && result_as_expected? && message_as_expected?
113
+ end
114
+
115
+ def because(message)
116
+ @message = message
117
+ self
118
+ end
119
+
120
+ def description
121
+ description = "halt while performing"
122
+ description += " because #{message}" if message
123
+ description += " when initialized with custom input (#{input_as_text})" if input
124
+ description += " and return the expected result (#{result_as_text})" if result
125
+ description
126
+ end
127
+
128
+ def failure_message
129
+ "the operation did not halt while performing for the following reason(s):\n#{failure_reasons}"
130
+ end
131
+
132
+ def failure_message_when_negated
133
+ "the operation was halted unexpectedly"
134
+ end
135
+
136
+ protected
137
+
138
+ def failure_reasons
139
+ reasons = []
140
+ reasons << "it did not halt at all" unless halted?
141
+ reasons << "its message was not as expected" unless message_as_expected?
142
+ unless result_as_expected?
143
+ reasons << [
144
+ "it did not return the expected result",
145
+ "Expected: #{result.inspect}",
146
+ "Got: #{operation.result.inspect}"
147
+ ].join("\n\t ")
148
+ end
149
+ reasons.map { |r| "\t- #{r}" }.join("\n")
150
+ end
151
+ end
152
+
153
+ def succeed_to_perform
154
+ SucceedToPerform.new
155
+ end
156
+
157
+ def halt_while_performing
158
+ HaltWhilePerforming.new
159
+ end
160
+ end
161
+ end
162
+ end
163
+
164
+ RSpec.configure do |config|
165
+ config.include ActiveOperation::Matcher::Execution
166
+ end
@@ -0,0 +1,90 @@
1
+ module ActiveOperation
2
+ module Matcher
3
+ module UtilizeOperation
4
+
5
+ class DummyOperation
6
+ def initialize(*args)
7
+ end
8
+
9
+ def halted?
10
+ false
11
+ end
12
+
13
+ def succeeded?
14
+ true
15
+ end
16
+
17
+ def output
18
+ Object.new
19
+ end
20
+
21
+ def perform
22
+ output
23
+ end
24
+ end
25
+
26
+ class Matcher
27
+ include RSpec::Mocks::ExampleMethods
28
+
29
+ attr_reader :composite_operations
30
+ attr_reader :tested_operation
31
+ attr_reader :tested_instance
32
+
33
+ def initialize(*composite_operations)
34
+ @composite_operations = composite_operations.flatten
35
+ end
36
+
37
+ def matches?(class_or_instance)
38
+ if class_or_instance.is_a?(Class)
39
+ @tested_operation = class_or_instance
40
+ @tested_instance = class_or_instance.new
41
+ else
42
+ @tested_operation = class_or_instance.class
43
+ @tested_instance = class_or_instance
44
+ end
45
+
46
+ allow(Base).to receive(:new).and_return(DummyOperation.new)
47
+ composite_operations.each do |composite_operation|
48
+ dummy_operation = DummyOperation.new
49
+ expect(dummy_operation).to receive(:perform).and_call_original
50
+ expect(composite_operation).to receive(:new).and_return(dummy_operation)
51
+ end
52
+ allow(tested_instance).to receive(:prepare).and_return(true)
53
+ allow(tested_instance).to receive(:finalize).and_return(true)
54
+ tested_instance.perform
55
+
56
+ tested_operation.operations == composite_operations
57
+ end
58
+
59
+ def description
60
+ "utilize the following operations: #{composite_operations.map(&:to_s).join(', ')}"
61
+ end
62
+
63
+ def failure_message
64
+ expected_but_not_used = composite_operations - tested_operation.operations
65
+ used_but_not_exptected = tested_operation.operations - composite_operations
66
+ message = ["Unexpected operation utilization:"]
67
+ message << "Expected: #{expected_but_not_used.join(', ')}" unless expected_but_not_used.empty?
68
+ message << "Not expected: #{used_but_not_exptected.join(', ')}" unless used_but_not_exptected.empty?
69
+ message.join("\n\t")
70
+ end
71
+
72
+ def failure_message_when_negated
73
+ "Unexpected operation utilization"
74
+ end
75
+ alias negative_failure_message failure_message_when_negated
76
+
77
+ end
78
+
79
+ def utilize_operation(*args)
80
+ Matcher.new(*args)
81
+ end
82
+ alias utilize_operations utilize_operation
83
+
84
+ end
85
+ end
86
+ end
87
+
88
+ RSpec.configure do |config|
89
+ config.include ActiveOperation::Matcher::UtilizeOperation
90
+ end
@@ -0,0 +1,94 @@
1
+ module ActiveOperation
2
+ class Pipeline < Base
3
+ class OperationFactory < SimpleDelegator
4
+ def initialize(operation_class, options = {})
5
+ super(operation_class)
6
+ @_options = options
7
+ end
8
+
9
+ def new(context, *input)
10
+ keyword_input_names = inputs.select(&:keyword?).map(&:name)
11
+ positional_args = input.shift(inputs.count(&:positional?))
12
+
13
+ attributes_from_input = input.last.kind_of?(Hash) ? input.pop.slice(*keyword_input_names) : {}
14
+ attributes_from_input.delete_if { |_, value| value.nil? }
15
+
16
+ attributes_from_pipeline = Array(@_options).each_with_object({}) do |(key, value), result|
17
+ result[key] = value.kind_of?(Proc) ? context.instance_exec(&value) : value
18
+ end
19
+
20
+ __getobj__.new *positional_args, attributes_from_input.merge(attributes_from_pipeline)
21
+ end
22
+ end
23
+
24
+ class << self
25
+ def operations
26
+ []
27
+ end
28
+
29
+ def use(operation, options = {})
30
+ if operations.empty?
31
+ inputs = operation.inputs
32
+
33
+ inputs.each do |input|
34
+ input input.name, type: input.type
35
+ end
36
+ end
37
+
38
+ (@operations ||= []) << OperationFactory.new(operation, options)
39
+ end
40
+
41
+ def compose(*operations, &block)
42
+ raise ArgumentError, "Expects either an array of operations or a block with configuration instructions" unless !!block ^ !operations.empty?
43
+
44
+ if block
45
+ Class.new(self, &block)
46
+ else
47
+ Class.new(self) do
48
+ operations.each do |operation|
49
+ use operation
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ protected
56
+
57
+ def inherited(subclass)
58
+ super
59
+
60
+ subclass.define_singleton_method(:operations) do
61
+ superclass.operations + Array(@operations)
62
+ end
63
+ end
64
+ end
65
+
66
+ protected
67
+
68
+ def execute
69
+ values = ->(input) { self[input.name] }
70
+
71
+ positional_arguments = self.class.inputs.select(&:positional?).map(&values)
72
+ keyword_arguments = self.class.inputs.select(&:keyword?).each_with_object({}) do |input, kwargs|
73
+ kwargs[input.name] = values.call(input)
74
+ end
75
+ arguments = positional_arguments.push(keyword_arguments)
76
+
77
+ self.class.operations.inject(arguments) do |data, operation|
78
+ operation = if data.respond_to?(:to_ary)
79
+ operation.new(self, *data)
80
+ else
81
+ operation.new(self, data)
82
+ end
83
+
84
+ operation.perform
85
+
86
+ output = operation.output
87
+
88
+ halt output if operation.halted?
89
+
90
+ output
91
+ end
92
+ end
93
+ end
94
+ end
@@ -1,3 +1,3 @@
1
1
  module ActiveOperation
2
- VERSION = "0.1.1"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,2 @@
1
+ Description:
2
+ Generates an application operation as a starting point for your operations.
@@ -0,0 +1,14 @@
1
+ require 'rails/generators/base'
2
+ require 'rails/generators/active_record'
3
+
4
+ module ActiveOperation
5
+ module Generators
6
+ class InstallGenerator < Rails::Generators::Base
7
+ source_root File.expand_path('../../../../../support/templates', __FILE__)
8
+
9
+ def create_application_operation
10
+ template 'application_operation.rb', 'app/operations/application_operation.rb'
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates an operation with the given name.
3
+
4
+ Example:
5
+ rails generate active_operation:operation Signup
6
+
7
+ This will create:
8
+ app/operations/signup.rb
@@ -0,0 +1,14 @@
1
+ require 'rails/generators/base'
2
+ require 'rails/generators/active_record'
3
+
4
+ module ActiveOperation
5
+ module Generators
6
+ class OperationGenerator < Rails::Generators::NamedBase
7
+ source_root File.expand_path('../../../../../support/templates', __FILE__)
8
+
9
+ def create_operation
10
+ template 'operation.rb', File.join('app/operations', class_path, "#{file_name}.rb")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,2 @@
1
+ class ApplicationOperation < ActiveOperation::Base
2
+ end
@@ -0,0 +1,17 @@
1
+ class <%= name %> < ApplicationOperation
2
+ # input :email, accepts: String, type: :keyword, required: true
3
+ # input :password, accepts: String, type: :keyword, required: true
4
+ #
5
+ # before do
6
+ # user = User.find_by(email: email)
7
+ # halt user unless user.nil?
8
+ # end
9
+ #
10
+ # def execute
11
+ # User.create!(email: email, password: password)
12
+ # end
13
+ #
14
+ # succeeded do
15
+ # Email::SendWelcomeMail.perform(output)
16
+ # end
17
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: active_operation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Konstantin Tennhard
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2016-09-15 00:00:00.000000000 Z
12
+ date: 2017-06-05 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -116,7 +116,17 @@ files:
116
116
  - lib/active_operation.rb
117
117
  - lib/active_operation/base.rb
118
118
  - lib/active_operation/input.rb
119
+ - lib/active_operation/matcher.rb
120
+ - lib/active_operation/matcher/execution.rb
121
+ - lib/active_operation/matcher/utilize_operation.rb
122
+ - lib/active_operation/pipeline.rb
119
123
  - lib/active_operation/version.rb
124
+ - lib/generators/active_operation/install/USAGE
125
+ - lib/generators/active_operation/install/install_generator.rb
126
+ - lib/generators/active_operation/operation/USAGE
127
+ - lib/generators/active_operation/operation/operation_generator.rb
128
+ - support/templates/application_operation.rb
129
+ - support/templates/operation.rb
120
130
  homepage: https://gitub.com/t6d/active_operation
121
131
  licenses:
122
132
  - MIT
@@ -137,7 +147,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
137
147
  version: '0'
138
148
  requirements: []
139
149
  rubyforge_project:
140
- rubygems_version: 2.5.1
150
+ rubygems_version: 2.6.11
141
151
  signing_key:
142
152
  specification_version: 4
143
153
  summary: ActiveOperation is a micro-framework for modelling business processes.