active_operation 0.1.1 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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.