core-pipeline 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
  SHA256:
3
- metadata.gz: 951f6c7809d29d8048837b0b24d497af8b1566c7990c9e0b296e556ff7b839ff
4
- data.tar.gz: 8151a7bff9a6cc159c604e6a15c9182e454a65f6aad577ea94ccf30e3d0c46d4
3
+ metadata.gz: 8a6b3ef16c4e8c95532c84fbcfeffa10e3d8b17b710f8a829ea0ea3735039195
4
+ data.tar.gz: e29f64c2b55dda1e75d022c4cca2f6a2c80fe00e13760ea678335abf74b57da4
5
5
  SHA512:
6
- metadata.gz: 56dd23992f750ff65a2585df138c93ae44e8d9cf2e5f511cc96ff0d1143d577f6b7035b45968f43f255bb5990ef0960de9106433e784f0481ae94b207d092acf
7
- data.tar.gz: 815e79983f3a73e589f1e0be6c170de6bc853ff541fdcdecfc1761cc572d5b1b626976af83b9278a234c755411d9ca02d11c8a238eba0f61b9d698077658fe06
6
+ metadata.gz: ccb44ec883364a2a633407dc257f44958386b817210278545e9b0291067437e553c6470616e53bab83876d04db2cebe0100ad727971a68aa955ebff7c3a87f9b
7
+ data.tar.gz: 6c1bb9fb0905caf54a3aef8b33bb224bc23779a66d66353b48345f61fe8a8e27c54653c65cf0d563f8073e387263675e9afe4ba0245bcfc9cf03fd8465a6b22b
data/CHANGELOG.md CHANGED
@@ -1,7 +1,15 @@
1
+ ## [v0.2.0](https://github.com/metabahn/corerb/releases/tag/2021-10-24)
2
+
3
+ *released on 2021-10-24*
4
+
5
+ * `chg` [#77](https://github.com/metabahn/corerb/pull/77) Add recompile support to pipelines ([bryanp](https://github.com/bryanp))
6
+
1
7
  ## [v0.1.0](https://github.com/metabahn/corerb/releases/tag/2021-07-15)
2
8
 
3
9
  *released on 2021-07-15*
4
10
 
11
+ * `fix` [#81](https://github.com/metabahn/corerb/pull/81) Tie up loose ends around curried pipeline actions ([bryanp](https://github.com/bryanp))
12
+ * `add` [#80](https://github.com/metabahn/corerb/pull/80) Define curried pipeline actions ([bryanp](https://github.com/bryanp))
5
13
  * `fix` [#69](https://github.com/metabahn/corerb/pull/69) Allow pipelines to be called at the class level ([bryanp](https://github.com/bryanp))
6
14
  * `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))
7
15
  * `chg` [#63](https://github.com/metabahn/corerb/pull/63) Improve inspect output for callable pipelines ([bryanp](https://github.com/bryanp))
@@ -15,21 +15,21 @@ module Core
15
15
  class << self
16
16
  # [public] Builds an action for a given target.
17
17
  #
18
- def build(target = nil, *args, before: nil, after: nil, context: nil, &block)
18
+ def build(target = nil, *args, before: nil, after: nil, context: nil, curry: false, &block)
19
19
  if block
20
- Actions::Block.new(target, before: before, after: after, context: context, &block)
20
+ Actions::Block.new(target, before: before, after: after, context: context, curry: curry, &block)
21
21
  else
22
- build_target(target, *args, before: before, after: after, context: context)
22
+ build_target(target, *args, before: before, after: after, context: context, curry: curry)
23
23
  end
24
24
  end
25
25
 
26
- private def build_target(first, *args, before:, after:, context:)
26
+ private def build_target(first, *args, before:, after:, context:, curry:)
27
27
  case first
28
28
  when String, Symbol
29
- if (target = build_target(args[0], *args[1..], before: before, after: after, context: context))
29
+ if (target = build_target(args[0], *args[1..], before: before, after: after, context: context, curry: curry))
30
30
  target
31
31
  else
32
- Actions::Method.new(first, before: before, after: after, context: context)
32
+ Actions::Method.new(first, before: before, after: after, context: context, curry: curry)
33
33
  end
34
34
  when NilClass
35
35
  nil
@@ -55,11 +55,12 @@ module Core
55
55
 
56
56
  include Is::Inspectable
57
57
 
58
- def initialize(name, before: nil, after: nil, context: nil)
58
+ def initialize(name, before: nil, after: nil, context: nil, curry: false)
59
59
  @name = (name || self.class.build_name).to_sym
60
60
  @before = before
61
61
  @after = after
62
62
  @context = context
63
+ @curry = curry
63
64
  end
64
65
 
65
66
  # [public] The action name.
@@ -74,6 +75,16 @@ module Core
74
75
  #
75
76
  attr_reader :after
76
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
+
77
88
  # [public] Finalizes the action for the given context.
78
89
  #
79
90
  def finalize(context)
@@ -10,10 +10,10 @@ module Core
10
10
  class Block < Action
11
11
  include Is::Inspectable
12
12
 
13
- def initialize(name = nil, before: nil, after: nil, context: nil, &block)
13
+ def initialize(name = nil, before: nil, after: nil, context: nil, curry: false, &block)
14
14
  @block = block
15
15
 
16
- super(name, before: before, after: after, context: context)
16
+ super(name, before: before, after: after, context: context, curry: curry)
17
17
  end
18
18
 
19
19
  # [public]
@@ -21,13 +21,19 @@ module Core
21
21
  def finalize(context)
22
22
  case @context
23
23
  when NilClass
24
- context.define_method(@name, &@block)
24
+ unless context.method_defined?(@name)
25
+ context.define_method(@name, &@block)
26
+ end
27
+
25
28
  @name
26
29
  else
27
30
  if @block.binding.receiver.equal?(@context)
28
31
  @block
29
32
  else
30
- @context.define_singleton_method(@name, &@block)
33
+ unless context.singleton_class.method_defined?(@name)
34
+ @context.define_singleton_method(@name, &@block)
35
+ end
36
+
31
37
  @context.method(@name).to_proc
32
38
  end
33
39
  end
@@ -10,10 +10,10 @@ module Core
10
10
  class Method < Action
11
11
  include Is::Inspectable
12
12
 
13
- def initialize(name = nil, before: nil, after: nil, context: nil)
13
+ def initialize(name = nil, before: nil, after: nil, context: nil, curry: false)
14
14
  @method = nil
15
15
 
16
- super(name, before: before, after: after, context: context)
16
+ super(name, before: before, after: after, context: context, curry: curry)
17
17
  end
18
18
 
19
19
  # [public]
@@ -18,11 +18,24 @@ module Core
18
18
  @mutex = Mutex.new
19
19
  @actions = []
20
20
  @skipped = []
21
+ @compiled = false
22
+ end
23
+
24
+ def initialize_copy(...)
25
+ @actions = @actions.clone
26
+ @skipped = @skipped.clone
27
+ super
21
28
  end
22
29
 
23
30
  attr_reader :actions
24
31
  attr_reader :skipped
25
32
 
33
+ # [public] Relocates to another object context.
34
+ #
35
+ def relocate(object)
36
+ @object = object
37
+ end
38
+
26
39
  # [public] Returns true if the pipeline will call any actions.
27
40
  #
28
41
  def any?
@@ -31,14 +44,16 @@ module Core
31
44
 
32
45
  # [public] Defines a pipeline action.
33
46
  #
34
- def action(*args, before: nil, after: nil, context: nil, &block)
35
- @actions << Action.build(*args, before: before, after: after, context: context, &block)
47
+ def action(*args, before: nil, after: nil, context: nil, curry: false, &block)
48
+ @actions << Action.build(*args, before: before, after: after, context: context, curry: curry, &block)
49
+ recompile if compiled?
36
50
  end
37
51
 
38
52
  # [public] Skips the given action.
39
53
  #
40
54
  def skip(name)
41
55
  @skipped << name.to_sym
56
+ recompile if compiled?
42
57
  end
43
58
 
44
59
  # [public] Replaces existing actions with actions from the given pipeline.
@@ -48,6 +63,7 @@ module Core
48
63
  @mutex.synchronize do
49
64
  @actions.clear
50
65
  @actions.concat(pipeline.pipeline.actions)
66
+ recompile if compiled?
51
67
  end
52
68
  else
53
69
  raise ArgumentError, "expected a pipeline"
@@ -59,6 +75,7 @@ module Core
59
75
  def include(pipeline)
60
76
  if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
61
77
  @actions.concat(pipeline.pipeline.actions)
78
+ recompile if compiled?
62
79
  else
63
80
  raise ArgumentError, "expected a pipeline"
64
81
  end
@@ -70,6 +87,7 @@ module Core
70
87
  if (pipeline.is_a?(Class) || pipeline.is_a?(Module)) && pipeline.ancestors.include?(Is::Pipeline)
71
88
  pipeline.pipeline.actions.each do |action|
72
89
  @actions.delete(action)
90
+ recompile if compiled?
73
91
  end
74
92
  else
75
93
  raise ArgumentError, "expected a pipeline"
@@ -84,9 +102,18 @@ module Core
84
102
 
85
103
  private def compile
86
104
  instance_eval(Compiler.compile(self, @object), __FILE__, __LINE__ - 1)
87
-
105
+ @compiled = true
88
106
  self
89
107
  end
108
+
109
+ private def recompile
110
+ singleton_class.remove_method(:call)
111
+ compile
112
+ end
113
+
114
+ private def compiled?
115
+ @compiled == true
116
+ end
90
117
  end
91
118
  end
92
119
  end
@@ -28,7 +28,11 @@ module Core
28
28
 
29
29
  compiled << case action
30
30
  when Symbol
31
- "object.#{action}(...); "
31
+ if object.private_method_defined?(action)
32
+ "object.send(#{action.inspect}, ...); "
33
+ else
34
+ "object.#{action}(...); "
35
+ end
32
36
  else
33
37
  "@__finalized_actions[#{object_id}].call(...); "
34
38
  end
@@ -57,7 +61,13 @@ module Core
57
61
  ordered_actions(callable).reject { |action|
58
62
  callable.skipped.include?(action.name)
59
63
  }.map { |action|
60
- action.finalize(object)
64
+ finalized = action.finalize(object)
65
+
66
+ if action.curried?
67
+ wrap_finalized_action_for_context(finalized, action.context || object)
68
+ else
69
+ finalized
70
+ end
61
71
  }.each_with_object({}) { |action, lookup|
62
72
  lookup[action.object_id] = action
63
73
  }
@@ -113,6 +123,205 @@ module Core
113
123
 
114
124
  ordered
115
125
  end
126
+
127
+ private def wrap_finalized_action_for_context(finalized, context)
128
+ case finalized
129
+ when Symbol
130
+ wrapped_name = :"curried_#{finalized}"
131
+ method = context.instance_method(finalized)
132
+ signature = [
133
+ build_method_arguments_signature(method),
134
+ build_method_keywords_signature(method),
135
+ "&block"
136
+ ].compact.join(", ")
137
+
138
+ code = <<~CODE
139
+ def #{wrapped_name}(*args, **kwargs, &block)
140
+ #{finalized}(#{signature})
141
+ end
142
+ CODE
143
+
144
+ context.class_eval(code, __FILE__, __LINE__ - 1)
145
+
146
+ wrapped_name
147
+ else
148
+ if callable_accepts_keyword_arguments?(finalized)
149
+ finalized_keywords = callable_keywords(finalized)
150
+
151
+ if callable_accepts_arguments?(finalized)
152
+ argument_count = callable_arguments(finalized).count
153
+
154
+ if callable_accepts_keyword_arguments_splat?(finalized)
155
+ if callable_accepts_arguments_splat?(finalized)
156
+ if RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
157
+ proc { |*args, **kwargs, &block|
158
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs.slice(*finalized_keywords), **kwargs.reject { |kwarg, _| finalized_keywords.include?(kwarg) }, &block)
159
+ }
160
+ else
161
+ proc { |*args, **kwargs, &block|
162
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
163
+ }
164
+ end
165
+ elsif RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
166
+ proc { |*args, **kwargs, &block|
167
+ finalized.call(*args.take(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), **kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
172
+ }
173
+ end
174
+ elsif callable_accepts_arguments_splat?(finalized)
175
+ proc { |*args, **kwargs, &block|
176
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs.slice(*finalized_keywords), &block)
177
+ }
178
+ else
179
+ proc { |*args, **kwargs, &block|
180
+ finalized.call(*args.take(argument_count), **kwargs.slice(*finalized_keywords), &block)
181
+ }
182
+ end
183
+ elsif callable_accepts_keyword_arguments_splat?(finalized)
184
+ if callable_accepts_arguments_splat?(finalized)
185
+ if RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
186
+ proc { |*args, **kwargs, &block|
187
+ finalized.call(*args, **kwargs.slice(*finalized_keywords), **kwargs.reject { |kwarg, _| finalized_keywords.include?(kwarg) }, &block)
188
+ }
189
+ else
190
+ proc { |*args, **kwargs, &block|
191
+ finalized.call(*args, **kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
192
+ }
193
+ end
194
+ elsif RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
195
+ proc { |*, **kwargs, &block|
196
+ finalized.call(**kwargs.slice(*finalized_keywords), **kwargs.reject { |kwarg, _| finalized_keywords.include?(kwarg) }, &block)
197
+ }
198
+ else
199
+ proc { |*, **kwargs, &block|
200
+ finalized.call(**kwargs.slice(*finalized_keywords), **kwargs.except(*finalized_keywords), &block)
201
+ }
202
+ end
203
+ elsif callable_accepts_arguments_splat?(finalized)
204
+ proc { |*args, **kwargs, &block|
205
+ finalized.call(*args, **kwargs.slice(*finalized_keywords), &block)
206
+ }
207
+ else
208
+ proc { |*, **kwargs, &block|
209
+ finalized.call(**kwargs.slice(*finalized_keywords), &block)
210
+ }
211
+ end
212
+ elsif callable_accepts_arguments?(finalized)
213
+ argument_count = callable_arguments(finalized).count
214
+
215
+ if callable_accepts_keyword_arguments_splat?(finalized)
216
+ if callable_accepts_arguments_splat?(finalized)
217
+ proc { |*args, **kwargs, &block|
218
+ finalized.call(*args.take(argument_count), *args[argument_count..], **kwargs, &block)
219
+ }
220
+ else
221
+ proc { |*args, **kwargs, &block|
222
+ finalized.call(*args.take(argument_count), **kwargs, &block)
223
+ }
224
+ end
225
+ elsif callable_accepts_arguments_splat?(finalized)
226
+ proc { |*args, **, &block|
227
+ finalized.call(*args.take(argument_count), *args[argument_count..], &block)
228
+ }
229
+ else
230
+ proc { |*args, **, &block|
231
+ finalized.call(*args.take(argument_count), &block)
232
+ }
233
+ end
234
+ elsif callable_accepts_keyword_arguments_splat?(finalized)
235
+ if callable_accepts_arguments_splat?(finalized)
236
+ proc { |*args, **kwargs, &block|
237
+ finalized.call(*args, **kwargs, &block)
238
+ }
239
+ else
240
+ proc { |*, **kwargs, &block|
241
+ finalized.call(**kwargs, &block)
242
+ }
243
+ end
244
+ elsif callable_accepts_arguments_splat?(finalized)
245
+ proc { |*args, **, &block|
246
+ finalized.call(*args, &block)
247
+ }
248
+ else
249
+ proc { |*, **, &block|
250
+ finalized.call(&block)
251
+ }
252
+ end
253
+ end
254
+ end
255
+
256
+ private def build_method_arguments_signature(method)
257
+ return if method.arity == 0
258
+
259
+ arguments = callable_arguments(method)
260
+
261
+ if arguments.any?
262
+ if callable_accepts_arguments_splat?(method)
263
+ "*args.take(#{arguments.count}), *args[#{arguments.count}..]"
264
+ else
265
+ "*args.take(#{arguments.count})"
266
+ end
267
+ elsif callable_accepts_arguments_splat?(method)
268
+ "*args"
269
+ end
270
+ end
271
+
272
+ private def build_method_keywords_signature(method)
273
+ return if method.arity == 0
274
+
275
+ keywords = callable_keywords(method)
276
+
277
+ if keywords.any?
278
+ keywords_string = keywords.map { |keyword| ":#{keyword}" }.join(", ")
279
+
280
+ if callable_accepts_keyword_arguments_splat?(method)
281
+ if RbConfig::CONFIG["RUBY_PROGRAM_VERSION"] < "3"
282
+ "**kwargs.slice(#{keywords_string}), **kwargs.reject { |kwarg, _| [#{keywords_string}].include?(kwarg) }"
283
+ else
284
+ "**kwargs.slice(#{keywords_string}), **kwargs.except(*#{keywords_string})"
285
+ end
286
+ else
287
+ "**kwargs.slice(#{keywords_string})"
288
+ end
289
+ elsif callable_accepts_keyword_arguments_splat?(method)
290
+ "**kwargs"
291
+ end
292
+ end
293
+
294
+ private def callable_accepts_arguments_splat?(callable)
295
+ callable.parameters.any? { |type, _| type == :rest }
296
+ end
297
+
298
+ private def callable_accepts_keyword_arguments_splat?(callable)
299
+ callable.parameters.any? { |type, _| type == :keyrest }
300
+ end
301
+
302
+ private def callable_accepts_arguments?(callable)
303
+ callable.parameters.any? { |type, _| type == :req || type == :opt }
304
+ end
305
+
306
+ private def callable_accepts_keyword_arguments?(callable)
307
+ callable.parameters.any? { |type, _| type == :keyreq || type == :key }
308
+ end
309
+
310
+ private def callable_arguments(callable)
311
+ callable.parameters.select { |type, _|
312
+ type == :req || type == :opt
313
+ }.map { |_, name|
314
+ name
315
+ }
316
+ end
317
+
318
+ private def callable_keywords(callable)
319
+ callable.parameters.select { |type, _|
320
+ type == :keyreq || type == :key
321
+ }.map { |_, name|
322
+ name
323
+ }
324
+ end
116
325
  end
117
326
  end
118
327
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module Core
4
4
  module Pipeline
5
- VERSION = "0.1.1"
5
+ VERSION = "0.2.0"
6
6
 
7
7
  def self.version
8
8
  VERSION
data/lib/is/pipeline.rb CHANGED
@@ -27,8 +27,8 @@ module Is
27
27
  extends :definition do
28
28
  # [public] Defines a pipeline action.
29
29
  #
30
- def action(*args, before: nil, after: nil, context: nil, &block)
31
- pipeline.action(*args, before: before, after: after, context: context, &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)
32
32
  end
33
33
 
34
34
  # [public] Skips the given action.
@@ -62,6 +62,41 @@ module Is
62
62
  def call(...)
63
63
  @pipeline.call(self, ...)
64
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
93
+
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)
99
+ end
65
100
  end
66
101
 
67
102
  extends :implementation, prepend: true do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: core-pipeline
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
  - Bryan Powell
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-16 00:00:00.000000000 Z
11
+ date: 2021-10-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: core-extension
@@ -89,7 +89,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  - !ruby/object:Gem::Version
90
90
  version: '0'
91
91
  requirements: []
92
- rubygems_version: 3.2.15
92
+ rubygems_version: 3.2.22
93
93
  signing_key:
94
94
  specification_version: 4
95
95
  summary: Turns Ruby objects into pipelines.