core-pipeline 0.0.0 → 0.3.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
  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.