core-pipeline 0.0.0 → 0.3.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
  SHA256:
3
- metadata.gz: a40a0de02118c468488220555a57d531b738170f51bd61bbe165f71f5851ddb6
4
- data.tar.gz: bd1d8c14350961d2011fad408748095c315d04b8e7d067332e259f0b9f644656
3
+ metadata.gz: 53ee7741ae5354263c5f7d426eb7ad9b500c01bb4a1ee4c665ce11d054819cea
4
+ data.tar.gz: 8c7e495b2681c54b59e38cc066987e0f52e3ba8a5673f8285c821a38eea98e9d
5
5
  SHA512:
6
- metadata.gz: 4b87d1a60bad4f85bbaedddcd956222c03380d957dcac7663c63ce56bda9c876f4a230907285982438ea33fc396b3c8de4497168c9eaa6eb451351a02444dbc9
7
- data.tar.gz: f9c54d75eb9f2803326d422ab7efed3f27bec648bdccaf9222633c16e1078bf4b01bb2a9086b7eb9954d3c76e17cef4c6ff4bdb4f65a521b6d5d0dd25235ee15
6
+ metadata.gz: 757e21b99dbb927b0d75f4dbd8f036e3235a982aa9a449260bcb4e4ff09f3a20afb788c3ddcd213d2a75d5b44e105f69bf512a6515991cc97282e584e4a617f7
7
+ data.tar.gz: 241b54f107db94c7fa5afeb1a075d2df1b3cd92c97bc54c9cb3935c283d6b2085d67eb1aeb85d2cb724b0cc6785f3bacb64062233ab50c5150f44bfd70603636
data/CHANGELOG.md CHANGED
@@ -1,3 +1,31 @@
1
+ ## [v0.3.0](https://github.com/metabahn/corerb/releases/tag/2021-11-02)
2
+
3
+ *released on 2021-11-02*
4
+
5
+ * `chg` [#97](https://github.com/metabahn/corerb/pull/97) Designate internal state with leading and trailing double underscores ([bryanp](https://github.com/bryanp))
6
+ * `chg` [#89](https://github.com/metabahn/corerb/pull/89) Allow the pipeline compiler to be extended ([bryanp](https://github.com/bryanp))
7
+
8
+ ## [v0.2.0](https://github.com/metabahn/corerb/releases/tag/2021-10-24)
9
+
10
+ *released on 2021-10-24*
11
+
12
+ * `chg` [#77](https://github.com/metabahn/corerb/pull/77) Add recompile support to pipelines ([bryanp](https://github.com/bryanp))
13
+
14
+ ## [v0.1.0](https://github.com/metabahn/corerb/releases/tag/2021-07-15)
15
+
16
+ *released on 2021-07-15*
17
+
18
+ * `fix` [#81](https://github.com/metabahn/corerb/pull/81) Tie up loose ends around curried pipeline actions ([bryanp](https://github.com/bryanp))
19
+ * `add` [#80](https://github.com/metabahn/corerb/pull/80) Define curried pipeline actions ([bryanp](https://github.com/bryanp))
20
+ * `fix` [#69](https://github.com/metabahn/corerb/pull/69) Allow pipelines to be called at the class level ([bryanp](https://github.com/bryanp))
21
+ * `add` [#64](https://github.com/metabahn/corerb/pull/64) Add a method for checking if a callable pipeline will call any actions ([bryanp](https://github.com/bryanp))
22
+ * `chg` [#63](https://github.com/metabahn/corerb/pull/63) Improve inspect output for callable pipelines ([bryanp](https://github.com/bryanp))
23
+ * `chg` [#61](https://github.com/metabahn/corerb/pull/61) Refactor pipelines to be more performant for new instances ([bryanp](https://github.com/bryanp))
24
+ * `fix` [#59](https://github.com/metabahn/corerb/pull/59) Prevent collisions in auto-defined action names ([bryanp](https://github.com/bryanp))
25
+ * `chg` [#58](https://github.com/metabahn/corerb/pull/58) Define pipeline actions with context ([bryanp](https://github.com/bryanp))
26
+ * `chg` [#57](https://github.com/metabahn/corerb/pull/57) Avoid cache lookups for action calls when the action is just a method ([bryanp](https://github.com/bryanp))
27
+ * `add` [#54](https://github.com/metabahn/corerb/pull/54) Make all pipeline objects inspectable ([bryanp](https://github.com/bryanp))
28
+
1
29
  ## [v0.0.0](https://github.com/metabahn/corerb/releases/tag/2021-07-07)
2
30
 
3
31
  *released on 2021-07-07*
@@ -1,28 +1,35 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "is/inspectable"
4
+ require "is/stateful"
5
+ require "securerandom"
6
+
3
7
  module Core
4
8
  module Pipeline
5
9
  # [public] A pipeline action.
6
10
  #
7
11
  class Action
12
+ include Is::Stateful
13
+ state :__used_random_suffixes__, default: []
14
+
8
15
  class << self
9
16
  # [public] Builds an action for a given target.
10
17
  #
11
- def build(target = nil, *args, context:, before:, after:, &block)
18
+ def build(target = nil, *args, before: nil, after: nil, context: nil, curry: false, &block)
12
19
  if block
13
- Actions::Block.new(target, before: before, after: after, &block)
20
+ Actions::Block.new(target, before: before, after: after, context: context, curry: curry, &block)
14
21
  else
15
- build_target(target, *args, before: before, after: after)
22
+ build_target(target, *args, before: before, after: after, context: context, curry: curry)
16
23
  end
17
24
  end
18
25
 
19
- private def build_target(first, *args, before:, after:)
26
+ private def build_target(first, *args, before:, after:, context:, curry:)
20
27
  case first
21
28
  when String, Symbol
22
- if (target = build_target(args[0], *args[1..], before: before, after: after))
29
+ if (target = build_target(args[0], *args[1..], before: before, after: after, context: context, curry: curry))
23
30
  target
24
31
  else
25
- Actions::Method.new(first, before: before, after: after)
32
+ Actions::Method.new(first, before: before, after: after, context: context, curry: curry)
26
33
  end
27
34
  when NilClass
28
35
  nil
@@ -30,11 +37,30 @@ module Core
30
37
  raise ArgumentError, "unsupported action type"
31
38
  end
32
39
  end
40
+
41
+ def build_name
42
+ suffix = generate_random_suffix
43
+ if __used_random_suffixes__.include?(suffix)
44
+ build_name
45
+ else
46
+ __used_random_suffixes__ << suffix
47
+ "action_#{suffix}"
48
+ end
49
+ end
50
+
51
+ def generate_random_suffix
52
+ SecureRandom.hex(8)
53
+ end
33
54
  end
34
55
 
35
- def initialize(before:, after:)
56
+ include Is::Inspectable
57
+
58
+ def initialize(name, before: nil, after: nil, context: nil, curry: false)
59
+ @name = (name || self.class.build_name).to_sym
36
60
  @before = before
37
61
  @after = after
62
+ @context = context
63
+ @curry = curry
38
64
  end
39
65
 
40
66
  # [public] The action name.
@@ -49,6 +75,16 @@ module Core
49
75
  #
50
76
  attr_reader :after
51
77
 
78
+ # [public] The context this action should be called in.
79
+ #
80
+ attr_reader :context
81
+
82
+ # [public] Returns `true` if the action should be curried.
83
+ #
84
+ def curried?
85
+ @curry == true
86
+ end
87
+
52
88
  # [public] Finalizes the action for the given context.
53
89
  #
54
90
  def finalize(context)
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "securerandom"
3
+ require "is/inspectable"
4
4
 
5
5
  module Core
6
6
  module Pipeline
@@ -8,24 +8,35 @@ module Core
8
8
  # [public] A pipeline action defined as a block.
9
9
  #
10
10
  class Block < Action
11
- class << self
12
- def build_name
13
- "action_#{SecureRandom.hex(8)}"
14
- end
15
- end
11
+ include Is::Inspectable
16
12
 
17
- def initialize(name = nil, before:, after:, &block)
18
- @name = name || self.class.build_name
13
+ def initialize(name = nil, before: nil, after: nil, context: nil, curry: false, &block)
19
14
  @block = block
20
15
 
21
- super(before: before, after: after)
16
+ super(name, before: before, after: after, context: context, curry: curry)
22
17
  end
23
18
 
24
19
  # [public]
25
20
  #
26
21
  def finalize(context)
27
- context.define_singleton_method(@name, &@block)
28
- context.method(@name).to_proc
22
+ case @context
23
+ when NilClass
24
+ unless context.method_defined?(@name)
25
+ context.define_method(@name, &@block)
26
+ end
27
+
28
+ @name
29
+ else
30
+ if @block.binding.receiver.equal?(@context)
31
+ @block
32
+ else
33
+ unless context.singleton_class.method_defined?(@name)
34
+ @context.define_singleton_method(@name, &@block)
35
+ end
36
+
37
+ @context.method(@name).to_proc
38
+ end
39
+ end
29
40
  end
30
41
  end
31
42
  end
@@ -1,22 +1,31 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "is/inspectable"
4
+
3
5
  module Core
4
6
  module Pipeline
5
7
  module Actions
6
8
  # [public] A pipeline action defined as a method.
7
9
  #
8
10
  class Method < Action
9
- def initialize(name = nil, before:, after:)
10
- @name = name
11
+ include Is::Inspectable
12
+
13
+ def initialize(name = nil, before: nil, after: nil, context: nil, curry: false)
11
14
  @method = nil
12
15
 
13
- super(before: before, after: after)
16
+ super(name, before: before, after: after, context: context, curry: curry)
14
17
  end
15
18
 
16
19
  # [public]
17
20
  #
18
21
  def finalize(context)
19
- context.method(@name).to_proc
22
+ case @context
23
+ when NilClass
24
+ context.instance_method(@name)
25
+ @name
26
+ else
27
+ @context.method(@name).to_proc
28
+ end
20
29
  end
21
30
  end
22
31
  end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "is/inspectable"
4
+
5
+ require_relative "action"
6
+ require_relative "compiler"
7
+
8
+ module Core
9
+ module Pipeline
10
+ include Is::Inspectable
11
+ inspects without: [:@object, :@mutex]
12
+
13
+ # [public] Calls a pipeline on a pipelined object.
14
+ #
15
+ class Callable
16
+ def initialize(object)
17
+ @object = object
18
+ @mutex = Mutex.new
19
+ @actions = []
20
+ @skipped = []
21
+ @compiled = false
22
+ @compiler = Compiler
23
+ end
24
+
25
+ def initialize_copy(...)
26
+ @actions = @actions.clone
27
+ @skipped = @skipped.clone
28
+ super
29
+ end
30
+
31
+ attr_reader :actions
32
+ attr_reader :skipped
33
+
34
+ # [public]
35
+ #
36
+ attr_writer :compiler
37
+
38
+ # [public] Relocates to another object context.
39
+ #
40
+ def relocate(object)
41
+ @object = object
42
+ end
43
+
44
+ # [public] Returns true if the pipeline will call any actions.
45
+ #
46
+ def any?
47
+ (@actions.map(&:name) - @skipped).any?
48
+ end
49
+
50
+ # [public] Defines a pipeline action.
51
+ #
52
+ def action(*args, before: nil, after: nil, context: nil, curry: false, &block)
53
+ @actions << Action.build(*args, before: before, after: after, context: context, curry: curry, &block)
54
+ recompile if compiled?
55
+ end
56
+
57
+ # [public] Skips the given action.
58
+ #
59
+ def skip(name)
60
+ @skipped << name.to_sym
61
+ recompile if compiled?
62
+ end
63
+
64
+ # [public] Replaces existing actions with actions from the given pipeline.
65
+ #
66
+ def use(pipeline)
67
+ if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
68
+ @mutex.synchronize do
69
+ @actions.clear
70
+ @actions.concat(pipeline.pipeline.actions)
71
+ recompile if compiled?
72
+ end
73
+ else
74
+ raise ArgumentError, "expected a pipeline"
75
+ end
76
+ end
77
+
78
+ # [public] Includes actions from the given pipeline.
79
+ #
80
+ def include(pipeline)
81
+ if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
82
+ @actions.concat(pipeline.pipeline.actions)
83
+ recompile if compiled?
84
+ else
85
+ raise ArgumentError, "expected a pipeline"
86
+ end
87
+ end
88
+
89
+ # [public] Excludes actions from the given pipeline.
90
+ #
91
+ def exclude(pipeline)
92
+ if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
93
+ pipeline.pipeline.actions.each do |action|
94
+ @actions.delete(action)
95
+ recompile if compiled?
96
+ end
97
+ else
98
+ raise ArgumentError, "expected a pipeline"
99
+ end
100
+ end
101
+
102
+ # [public] Calls the pipeline in context of an object with the given arguments.
103
+ #
104
+ def call(object, ...)
105
+ compile.call(object, ...)
106
+ end
107
+
108
+ private def compile
109
+ instance_eval(@compiler.compile(self, @object), __FILE__, __LINE__ - 1)
110
+ @compiled = true
111
+ self
112
+ end
113
+
114
+ private def recompile
115
+ singleton_class.remove_method(:call)
116
+ compile
117
+ end
118
+
119
+ private def compiled?
120
+ @compiled == true
121
+ end
122
+ end
123
+ end
124
+ end
@@ -5,30 +5,29 @@ require_relative "signals/rejected"
5
5
 
6
6
  module Core
7
7
  module Pipeline
8
- # [public] Compiles a pipeline instance into a callable object.
8
+ # [public] Compiles a pipeline into evalable code.
9
9
  #
10
10
  class Compiler
11
11
  class << self
12
- # [public] Compiles a pipeline instance.
12
+ # [public] Compiles a callable pipeline in context of an object.
13
13
  #
14
- def compile(pipeline)
15
- compiled = <<~CODE
16
- def call(...)
17
- #{compile_pipeline(pipeline)}
14
+ def compile(callable, object)
15
+ <<~CODE
16
+ def call(object, ...)
17
+ #{compile_call(callable, object)}
18
18
  rescue Core::Pipeline::Signals::Halted => error
19
19
  error.value
20
20
  end
21
21
  CODE
22
-
23
- pipeline.instance_eval(compiled, __FILE__, __LINE__ - 1)
24
22
  end
25
23
 
26
- private def compile_pipeline(pipeline)
24
+ # [public]
25
+ #
26
+ private def compile_call(callable, object)
27
27
  compiled = +""
28
-
29
- finalized_actions(pipeline).each_key do |object_id|
28
+ finalized_actions(callable, object).each_value do |action|
30
29
  compiled << "begin\n"
31
- compiled << " @__finalized_actions[#{object_id}].call(...)\n"
30
+ compiled << compile_action(action, object)
32
31
  compiled << "rescue Core::Pipeline::Signals::Rejected\n"
33
32
  compiled << "end\n"
34
33
  end
@@ -36,38 +35,61 @@ module Core
36
35
  compiled
37
36
  end
38
37
 
39
- private def finalized_actions(pipeline)
40
- validate_skipped_actions(pipeline)
41
- finalized_actions = build_finalized_actions(pipeline)
42
- pipeline.instance_variable_set(:@__finalized_actions, finalized_actions)
38
+ # [public]
39
+ #
40
+ private def compile_action(action, object)
41
+ case action[:finalized]
42
+ when Symbol
43
+ if object.private_method_defined?(action[:finalized])
44
+ "object.send(#{action[:finalized].inspect}, ...)\n"
45
+ else
46
+ "object.#{action[:finalized]}(...)\n"
47
+ end
48
+ else
49
+ "@__finalized_actions__[#{action[:object_id]}][:finalized].call(...)\n"
50
+ end
51
+ end
52
+
53
+ private def finalized_actions(callable, object)
54
+ validate_skipped_actions(callable)
55
+ finalized_actions = build_finalized_actions(callable, object)
56
+ callable.instance_variable_set(:@__finalized_actions__, finalized_actions)
43
57
  end
44
58
 
45
- private def validate_skipped_actions(pipeline)
46
- pipeline.__skipped_actions.each do |skipped_action|
47
- unless pipeline.__actions.any? { |action| action.name == skipped_action }
48
- raise "cannot skip unknown action `#{skipped_action}`"
59
+ private def validate_skipped_actions(callable)
60
+ callable.skipped.each do |skipped|
61
+ unless callable.actions.any? { |action| action.name == skipped }
62
+ raise "cannot skip unknown action `#{skipped}`"
49
63
  end
50
64
  end
51
65
  end
52
66
 
53
- private def build_finalized_actions(pipeline)
54
- ordered_actions(pipeline).reject { |action|
55
- pipeline.__skipped_actions.include?(action.name)
67
+ private def build_finalized_actions(callable, object)
68
+ ordered_actions(callable).reject { |action|
69
+ callable.skipped.include?(action.name)
56
70
  }.map { |action|
57
- action.finalize(pipeline)
71
+ finalized = action.finalize(object)
72
+
73
+ finalized = if action.curried?
74
+ wrap_finalized_action_for_context(finalized, action.context || object)
75
+ else
76
+ finalized
77
+ end
78
+
79
+ {object: action, finalized: finalized, object_id: finalized.object_id}
58
80
  }.each_with_object({}) { |action, lookup|
59
- lookup[action.object_id] = action
81
+ lookup[action[:object_id]] = action
60
82
  }
61
83
  end
62
84
 
63
- private def ordered_actions(pipeline)
64
- return pipeline.__actions if pipeline.__actions.empty?
85
+ private def ordered_actions(callable)
86
+ return callable.actions if callable.actions.empty?
65
87
 
66
88
  marked = []
67
89
  ordered = []
68
- working = pipeline.__actions.dup
90
+ working = callable.actions.dup
69
91
 
70
- pipeline.__actions.each do |action|
92
+ callable.actions.each do |action|
71
93
  unless action.before || action.after
72
94
  ordered << action
73
95
  working.delete(action)
@@ -110,6 +132,205 @@ module Core
110
132
 
111
133
  ordered
112
134
  end
135
+
136
+ private def wrap_finalized_action_for_context(finalized, context)
137
+ case finalized
138
+ when Symbol
139
+ wrapped_name = :"curried_#{finalized}"
140
+ method = context.instance_method(finalized)
141
+ signature = [
142
+ build_method_arguments_signature(method),
143
+ build_method_keywords_signature(method),
144
+ "&block"
145
+ ].compact.join(", ")
146
+
147
+ code = <<~CODE
148
+ def #{wrapped_name}(*args, **kwargs, &block)
149
+ self.#{finalized}(#{signature})
150
+ end
151
+ CODE
152
+
153
+ context.class_eval(code, __FILE__, __LINE__ - 1)
154
+
155
+ wrapped_name
156
+ else
157
+ if callable_accepts_keyword_arguments?(finalized)
158
+ finalized_keywords = callable_keywords(finalized)
159
+
160
+ if callable_accepts_arguments?(finalized)
161
+ argument_count = callable_arguments(finalized).count
162
+
163
+ if callable_accepts_keyword_arguments_splat?(finalized)
164
+ if callable_accepts_arguments_splat?(finalized)
165
+ if RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
166
+ proc { |*args, **kwargs, &block|
167
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs.slice(*finalized_keywords), **kwargs.reject { |kwarg, _| finalized_keywords.include?(kwarg) }, &block)
168
+ }
169
+ else
170
+ proc { |*args, **kwargs, &block|
171
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
172
+ }
173
+ end
174
+ elsif RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
175
+ proc { |*args, **kwargs, &block|
176
+ finalized.call(*args.take(argument_count), **kwargs.slice(*finalized_keywords), **kwargs.reject { |kwarg, _| finalized_keywords.include?(kwarg) }, &block)
177
+ }
178
+ else
179
+ proc { |*args, **kwargs, &block|
180
+ finalized.call(*args.take(argument_count), **kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
181
+ }
182
+ end
183
+ elsif callable_accepts_arguments_splat?(finalized)
184
+ proc { |*args, **kwargs, &block|
185
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs.slice(*finalized_keywords), &block)
186
+ }
187
+ else
188
+ proc { |*args, **kwargs, &block|
189
+ finalized.call(*args.take(argument_count), **kwargs.slice(*finalized_keywords), &block)
190
+ }
191
+ end
192
+ elsif callable_accepts_keyword_arguments_splat?(finalized)
193
+ if callable_accepts_arguments_splat?(finalized)
194
+ if RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
195
+ proc { |*args, **kwargs, &block|
196
+ finalized.call(*args, **kwargs.slice(*finalized_keywords), **kwargs.reject { |kwarg, _| finalized_keywords.include?(kwarg) }, &block)
197
+ }
198
+ else
199
+ proc { |*args, **kwargs, &block|
200
+ finalized.call(*args, **kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
201
+ }
202
+ end
203
+ elsif RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
204
+ proc { |*, **kwargs, &block|
205
+ finalized.call(**kwargs.slice(*finalized_keywords), **kwargs.reject { |kwarg, _| finalized_keywords.include?(kwarg) }, &block)
206
+ }
207
+ else
208
+ proc { |*, **kwargs, &block|
209
+ finalized.call(**kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
210
+ }
211
+ end
212
+ elsif callable_accepts_arguments_splat?(finalized)
213
+ proc { |*args, **kwargs, &block|
214
+ finalized.call(*args, **kwargs.slice(*finalized_keywords), &block)
215
+ }
216
+ else
217
+ proc { |*, **kwargs, &block|
218
+ finalized.call(**kwargs.slice(*finalized_keywords), &block)
219
+ }
220
+ end
221
+ elsif callable_accepts_arguments?(finalized)
222
+ argument_count = callable_arguments(finalized).count
223
+
224
+ if callable_accepts_keyword_arguments_splat?(finalized)
225
+ if callable_accepts_arguments_splat?(finalized)
226
+ proc { |*args, **kwargs, &block|
227
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs, &block)
228
+ }
229
+ else
230
+ proc { |*args, **kwargs, &block|
231
+ finalized.call(*args.take(argument_count), **kwargs, &block)
232
+ }
233
+ end
234
+ elsif callable_accepts_arguments_splat?(finalized)
235
+ proc { |*args, **, &block|
236
+ finalized.call(*args.take(argument_count), *args[argument_count..], &block)
237
+ }
238
+ else
239
+ proc { |*args, **, &block|
240
+ finalized.call(*args.take(argument_count), &block)
241
+ }
242
+ end
243
+ elsif callable_accepts_keyword_arguments_splat?(finalized)
244
+ if callable_accepts_arguments_splat?(finalized)
245
+ proc { |*args, **kwargs, &block|
246
+ finalized.call(*args, **kwargs, &block)
247
+ }
248
+ else
249
+ proc { |*, **kwargs, &block|
250
+ finalized.call(**kwargs, &block)
251
+ }
252
+ end
253
+ elsif callable_accepts_arguments_splat?(finalized)
254
+ proc { |*args, **, &block|
255
+ finalized.call(*args, &block)
256
+ }
257
+ else
258
+ proc { |*, **, &block|
259
+ finalized.call(&block)
260
+ }
261
+ end
262
+ end
263
+ end
264
+
265
+ private def build_method_arguments_signature(method)
266
+ return if method.arity == 0
267
+
268
+ arguments = callable_arguments(method)
269
+
270
+ if arguments.any?
271
+ if callable_accepts_arguments_splat?(method)
272
+ "*args.take(#{arguments.count}), *args[#{arguments.count}..]"
273
+ else
274
+ "*args.take(#{arguments.count})"
275
+ end
276
+ elsif callable_accepts_arguments_splat?(method)
277
+ "*args"
278
+ end
279
+ end
280
+
281
+ private def build_method_keywords_signature(method)
282
+ return if method.arity == 0
283
+
284
+ keywords = callable_keywords(method)
285
+
286
+ if keywords.any?
287
+ keywords_string = keywords.map { |keyword| ":#{keyword}" }.join(", ")
288
+
289
+ if callable_accepts_keyword_arguments_splat?(method)
290
+ if RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
291
+ "**kwargs.slice(#{keywords_string}), **kwargs.reject { |kwarg, _| [#{keywords_string}].include?(kwarg) }"
292
+ else
293
+ "**kwargs.slice(#{keywords_string}), **kwargs.except(*#{keywords_string})"
294
+ end
295
+ else
296
+ "**kwargs.slice(#{keywords_string})"
297
+ end
298
+ elsif callable_accepts_keyword_arguments_splat?(method)
299
+ "**kwargs"
300
+ end
301
+ end
302
+
303
+ private def callable_accepts_arguments_splat?(callable)
304
+ callable.parameters.any? { |type, _| type == :rest }
305
+ end
306
+
307
+ private def callable_accepts_keyword_arguments_splat?(callable)
308
+ callable.parameters.any? { |type, _| type == :keyrest }
309
+ end
310
+
311
+ private def callable_accepts_arguments?(callable)
312
+ callable.parameters.any? { |type, _| type == :req || type == :opt }
313
+ end
314
+
315
+ private def callable_accepts_keyword_arguments?(callable)
316
+ callable.parameters.any? { |type, _| type == :keyreq || type == :key }
317
+ end
318
+
319
+ private def callable_arguments(callable)
320
+ callable.parameters.select { |type, _|
321
+ type == :req || type == :opt
322
+ }.map { |_, name|
323
+ name
324
+ }
325
+ end
326
+
327
+ private def callable_keywords(callable)
328
+ callable.parameters.select { |type, _|
329
+ type == :keyreq || type == :key
330
+ }.map { |_, name|
331
+ name
332
+ }
333
+ end
113
334
  end
114
335
  end
115
336
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "is/inspectable"
4
+
3
5
  module Core
4
6
  module Pipeline
5
7
  module Signals
6
8
  class Halted < Exception
9
+ include Is::Inspectable
10
+
7
11
  def initialize(value)
8
12
  @value = value
9
13
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "is/inspectable"
4
+
3
5
  module Core
4
6
  module Pipeline
5
7
  module Signals
6
8
  class Rejected < Exception
9
+ include Is::Inspectable
7
10
  end
8
11
  end
9
12
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Core
4
4
  module Pipeline
5
- VERSION = "0.0.0"
5
+ VERSION = "0.3.0"
6
6
 
7
7
  def self.version
8
8
  VERSION
data/lib/is/pipeline.rb CHANGED
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "is/async"
4
3
  require "is/extension"
5
4
  require "is/stateful"
6
5
 
7
- require_relative "../core/pipeline/action"
6
+ require_relative "../core/pipeline/callable"
8
7
  require_relative "../core/pipeline/compiler"
9
8
  require_relative "../core/pipeline/signals/halted"
10
9
  require_relative "../core/pipeline/signals/rejected"
@@ -15,70 +14,92 @@ module Is
15
14
  module Pipeline
16
15
  extend Is::Extension
17
16
 
17
+ applies do |extended:|
18
+ # [public] The current callable pipeline.
19
+ #
20
+ state :pipeline, default: Core::Pipeline::Callable.new(extended ? singleton_class : self)
21
+ end
22
+
18
23
  extends dependencies: [
19
24
  Is::Stateful
20
25
  ]
21
26
 
22
- extends :class do
27
+ extends :definition do
23
28
  # [public] Defines a pipeline action.
24
29
  #
25
- def action(*args, before: nil, after: nil, &block)
26
- @__actions << Core::Pipeline::Action.build(*args, context: self, before: before, after: after, &block)
30
+ def action(*args, before: nil, after: nil, context: nil, curry: false, &block)
31
+ pipeline.action(*args, before: before, after: after, context: context, curry: curry, &block)
27
32
  end
28
33
 
29
34
  # [public] Skips the given action.
30
35
  #
31
36
  def skip_action(name)
32
- @__skipped_actions << name.to_sym
37
+ pipeline.skip(name)
33
38
  end
34
39
 
35
40
  # [public] Replaces existing actions with actions from the given pipeline.
36
41
  #
37
- def use_pipeline(pipeline)
38
- if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
39
- @__actions.clear
40
- @__actions.concat(pipeline.__actions)
41
- else
42
- raise ArgumentError, "expected a pipeline"
43
- end
42
+ def use_pipeline(other_pipeline)
43
+ pipeline.use(other_pipeline)
44
44
  end
45
45
 
46
46
  # [public] Includes actions from the given pipeline.
47
47
  #
48
- def include_pipeline(pipeline)
49
- if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
50
- @__actions.concat(pipeline.__actions)
51
- else
52
- raise ArgumentError, "expected a pipeline"
53
- end
48
+ def include_pipeline(other_pipeline)
49
+ pipeline.include(other_pipeline)
54
50
  end
55
51
 
56
52
  # [public] Excludes actions from the given pipeline.
57
53
  #
58
- def exclude_pipeline(pipeline)
59
- if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
60
- pipeline.__actions.each do |action|
61
- @__actions.delete(action)
62
- end
63
- else
64
- raise ArgumentError, "expected a pipeline"
65
- end
54
+ def exclude_pipeline(other_pipeline)
55
+ pipeline.exclude(other_pipeline)
66
56
  end
67
57
  end
68
58
 
69
- extends :instance do
59
+ extends :implementation do
70
60
  # [public] Calls a pipeline with arguments.
71
61
  #
72
62
  def call(...)
73
- # Compiles the pipeline then redefines `call` to call the compiled pipeline.
74
- #
75
- Core::Pipeline::Compiler.compile(self)
63
+ @pipeline.call(self, ...)
64
+ end
65
+
66
+ # [public] Defines a pipeline action, isolated to the instance.
67
+ #
68
+ def action(*args, before: nil, after: nil, context: nil, curry: false, &block)
69
+ pipeline.relocate(singleton_class)
70
+ pipeline.action(*args, before: before, after: after, context: context, curry: curry, &block)
71
+ end
72
+
73
+ # [public] Skips the given action, isolated to the instance.
74
+ #
75
+ def skip_action(name)
76
+ pipeline.relocate(singleton_class)
77
+ pipeline.skip(name)
78
+ end
79
+
80
+ # [public] Replaces existing actions with actions from the given pipeline, isolated to the instance.
81
+ #
82
+ def use_pipeline(other_pipeline)
83
+ pipeline.relocate(singleton_class)
84
+ pipeline.use(other_pipeline)
85
+ end
86
+
87
+ # [public] Includes actions from the given pipeline, isolated to the instance.
88
+ #
89
+ def include_pipeline(other_pipeline)
90
+ pipeline.relocate(singleton_class)
91
+ pipeline.include(other_pipeline)
92
+ end
76
93
 
77
- call(...)
94
+ # [public] Excludes actions from the given pipeline, isolated to the instance.
95
+ #
96
+ def exclude_pipeline(other_pipeline)
97
+ pipeline.relocate(singleton_class)
98
+ pipeline.exclude(other_pipeline)
78
99
  end
79
100
  end
80
101
 
81
- extends :instance, prepend: true do
102
+ extends :implementation, prepend: true do
82
103
  # [public] Halts the execution of the pipeline, setting the pipeline's current value to the given object.
83
104
  #
84
105
  private def halt(value = nil)
@@ -91,10 +112,5 @@ module Is
91
112
  raise Core::Pipeline::Signals::Rejected
92
113
  end
93
114
  end
94
-
95
- applied do
96
- state :__actions, default: []
97
- state :__skipped_actions, default: []
98
- end
99
115
  end
100
116
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: core-pipeline
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Bryan Powell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-07 00:00:00.000000000 Z
11
+ date: 2021-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
- name: core-async
14
+ name: core-extension
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0.6'
19
+ version: '0.3'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0.6'
26
+ version: '0.3'
27
27
  - !ruby/object:Gem::Dependency
28
- name: core-extension
28
+ name: core-inspect
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - "~>"
32
32
  - !ruby/object:Gem::Version
33
- version: '0.1'
33
+ version: '0.0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
- version: '0.1'
40
+ version: '0.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: core-state
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -64,6 +64,7 @@ files:
64
64
  - lib/core/pipeline/action.rb
65
65
  - lib/core/pipeline/actions/block.rb
66
66
  - lib/core/pipeline/actions/method.rb
67
+ - lib/core/pipeline/callable.rb
67
68
  - lib/core/pipeline/compiler.rb
68
69
  - lib/core/pipeline/signals/halted.rb
69
70
  - lib/core/pipeline/signals/rejected.rb
@@ -88,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
88
89
  - !ruby/object:Gem::Version
89
90
  version: '0'
90
91
  requirements: []
91
- rubygems_version: 3.2.15
92
+ rubygems_version: 3.2.22
92
93
  signing_key:
93
94
  specification_version: 4
94
95
  summary: Turns Ruby objects into pipelines.