callable_tree 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 3d3a8425d7d4634140f8ae67d37b00212909bfefb2055fd75d98e8d6f851b5be
4
+ data.tar.gz: 0103f8cda6b99b7f122b79f95f6b70bfdd8c8b5aecc01791e870941b0d5d2f36
5
+ SHA512:
6
+ metadata.gz: 6722db751a4b784f8983aa9164371bf9f933c3ea7a1e00b3e0595970a3842f93fd86357da48fc6958babdb7add5d0018d5ace986049ae286961bf328ddcc36ab
7
+ data.tar.gz: 74fd411e58ef8b04d810504f2003809f50488e6fabcc8ff9ff4b596e509b750eaeb4e333698d259dd6a39ce3293f84f2755bcaa3d296a5364d17bbb315128ff9
@@ -0,0 +1,25 @@
1
+ name: build
2
+ on: [push]
3
+ jobs:
4
+ build:
5
+ runs-on: ubuntu-latest
6
+ strategy:
7
+ matrix:
8
+ ruby: ['2.4', '2.5', '2.6', '2.7', '3.0']
9
+ steps:
10
+ - uses: actions/checkout@v2
11
+ - uses: ruby/setup-ruby@v1
12
+ with:
13
+ ruby-version: ${{ matrix.ruby }}
14
+ - run: gem install bundler:2.2.4
15
+ - uses: actions/cache@v2
16
+ with:
17
+ path: vendor/bundle
18
+ key: ${{ runner.os }}-ruby-${{ matrix.ruby }}-gems-${{ hashFiles('**/Gemfile.lock') }}
19
+ restore-keys: |
20
+ ${{ runner.os }}-gems-
21
+ - name: Run bundle install
22
+ run: |
23
+ bundle config path vendor/bundle
24
+ bundle install --jobs 4 --retry 3
25
+ - run: bundle exec rspec
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 3.0.0
data/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ ## [Unreleased]
2
+
3
+ ## [0.1.0] - 2021-05-19
4
+
5
+ - Initial release
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in callable_tree.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'rspec', '~> 3.0'
data/Gemfile.lock ADDED
@@ -0,0 +1,35 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ callable_tree (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ diff-lcs (1.4.4)
10
+ rake (13.0.3)
11
+ rspec (3.10.0)
12
+ rspec-core (~> 3.10.0)
13
+ rspec-expectations (~> 3.10.0)
14
+ rspec-mocks (~> 3.10.0)
15
+ rspec-core (3.10.1)
16
+ rspec-support (~> 3.10.0)
17
+ rspec-expectations (3.10.1)
18
+ diff-lcs (>= 1.2.0, < 2.0)
19
+ rspec-support (~> 3.10.0)
20
+ rspec-mocks (3.10.2)
21
+ diff-lcs (>= 1.2.0, < 2.0)
22
+ rspec-support (~> 3.10.0)
23
+ rspec-support (3.10.2)
24
+
25
+ PLATFORMS
26
+ x86_64-darwin-19
27
+ x86_64-darwin-20
28
+
29
+ DEPENDENCIES
30
+ callable_tree!
31
+ rake (~> 13.0)
32
+ rspec (~> 3.0)
33
+
34
+ BUNDLED WITH
35
+ 2.2.17
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2021  
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,436 @@
1
+ # CallableTree
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'callable_tree'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle install
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install callable_tree
18
+
19
+ ## Usage
20
+
21
+ - `CallableTree::Node::Internal`
22
+ - This `module` is used to define a node that can have child nodes.
23
+ - `CallableTree::Node::External`
24
+ - This `module` is used to define a leaf node that cannot have child nodes.
25
+ - `CallableTree::Node::Root`
26
+ - This `class` includes `CallableTree::Node::Internal`. When there is no need to customize the internal node, use this `class`.
27
+
28
+ Builds a tree by linking instances of the nodes. The `call` method of the node where the `match?` method returns a truthy value is called in a chain from the root node to the leaf node.
29
+ If the `call` method returns a value other than `nil`, the next sibling node does not be called. This behavior is changeable by overriding the `terminate?` method.
30
+
31
+ ### Basic
32
+
33
+ `examples/example1.rb`:
34
+ ```ruby
35
+ module Node
36
+ module JSON
37
+ class Parser
38
+ include CallableTree::Node::Internal
39
+
40
+ def match?(input, **options)
41
+ File.extname(input) == '.json'
42
+ end
43
+
44
+ # If there is need to convert the input value for
45
+ # child nodes, override the `call` method.
46
+ def call(input, **options)
47
+ File.open(input) do |file|
48
+ json = ::JSON.load(file)
49
+ super(json, **options)
50
+ end
51
+ end
52
+
53
+ # If a returned value of the `call` method is `nil`,
54
+ # but there is no need to call the sibling nodes,
55
+ # override the `terminate?` method to return `true`.
56
+ def terminate?(output, **options)
57
+ true
58
+ end
59
+ end
60
+
61
+ class Scraper
62
+ include CallableTree::Node::External
63
+
64
+ def initialize(type:)
65
+ @type = type
66
+ end
67
+
68
+ def match?(input, **options)
69
+ !!input[@type.to_s]
70
+ end
71
+
72
+ def call(input, **options)
73
+ input[@type.to_s]
74
+ .map { |element| [element['name'], element['emoji']] }
75
+ .to_h
76
+ end
77
+ end
78
+ end
79
+
80
+ module XML
81
+ class Parser
82
+ include CallableTree::Node::Internal
83
+
84
+ def match?(input, **options)
85
+ File.extname(input) == '.xml'
86
+ end
87
+
88
+ # If there is need to convert the input value for
89
+ # child nodes, override the `call` method.
90
+ def call(input, **options)
91
+ File.open(input) do |file|
92
+ super(REXML::Document.new(file), **options)
93
+ end
94
+ end
95
+
96
+ # If a returned value of the `call` method is `nil`,
97
+ # but there is no need to call the sibling nodes,
98
+ # override the `terminate?` method to return `true`.
99
+ def terminate?(output, **options)
100
+ true
101
+ end
102
+ end
103
+
104
+ class Scraper
105
+ include CallableTree::Node::External
106
+
107
+ def initialize(type:)
108
+ @type = type
109
+ end
110
+
111
+ def match?(input, **options)
112
+ !input.get_elements("//#{@type}").empty?
113
+ end
114
+
115
+ def call(input, **options)
116
+ input
117
+ .get_elements("//#{@type}")
118
+ .first
119
+ .map { |element| [element['name'], element['emoji']] }
120
+ .to_h
121
+ end
122
+ end
123
+ end
124
+ end
125
+
126
+ tree = CallableTree::Node::Root.new.append(
127
+ Node::JSON::Parser.new.append(
128
+ Node::JSON::Scraper.new(type: :animals),
129
+ Node::JSON::Scraper.new(type: :fruits)
130
+ ),
131
+ Node::XML::Parser.new.append(
132
+ Node::XML::Scraper.new(type: :animals),
133
+ Node::XML::Scraper.new(type: :fruits)
134
+ )
135
+ )
136
+
137
+ Dir.glob(__dir__ + '/docs/*') do |file|
138
+ options = { foo: :bar }
139
+ puts tree.call(file, **options)
140
+ puts '---'
141
+ end
142
+ ```
143
+
144
+ Run `examples/example1.rb`:
145
+ ```sh
146
+ % ruby examples/example1.rb
147
+ {"Dog"=>"🐶", "Cat"=>"🐱"}
148
+ ---
149
+ {"Dog"=>"🐶", "Cat"=>"🐱"}
150
+ ---
151
+ {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
152
+ ---
153
+ {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
154
+ ---
155
+ ```
156
+
157
+ ### Advanced
158
+
159
+ #### `CallableTree::Node::External#verbosify`
160
+
161
+ If you want verbose result, call it.
162
+
163
+ `examples/example2.rb`:
164
+ ```ruby
165
+ ...
166
+
167
+ tree = CallableTree::Node::Root.new.append(
168
+ Node::JSON::Parser.new.append(
169
+ Node::JSON::Scraper.new(type: :animals).verbosify,
170
+ Node::JSON::Scraper.new(type: :fruits).verbosify
171
+ ),
172
+ Node::XML::Parser.new.append(
173
+ Node::XML::Scraper.new(type: :animals).verbosify,
174
+ Node::XML::Scraper.new(type: :fruits).verbosify
175
+ )
176
+ )
177
+
178
+ ...
179
+ ```
180
+
181
+ Run `examples/example2.rb`:
182
+ ```sh
183
+ % ruby examples/example2.rb
184
+ #<struct CallableTree::Node::External::Output value={"Dog"=>"🐶", "Cat"=>"🐱"}, options={:foo=>:bar}, routes=[Node::JSON::Scraper, Node::JSON::Parser, CallableTree::Node::Root]>
185
+ ---
186
+ #<struct CallableTree::Node::External::Output value={"Dog"=>"🐶", "Cat"=>"🐱"}, options={:foo=>:bar}, routes=[Node::XML::Scraper, Node::XML::Parser, CallableTree::Node::Root]>
187
+ ---
188
+ #<struct CallableTree::Node::External::Output value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"}, options={:foo=>:bar}, routes=[Node::JSON::Scraper, Node::JSON::Parser, CallableTree::Node::Root]>
189
+ ---
190
+ #<struct CallableTree::Node::External::Output value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"}, options={:foo=>:bar}, routes=[Node::XML::Scraper, Node::XML::Parser, CallableTree::Node::Root]>
191
+ ---
192
+ ```
193
+
194
+ At first glance, this looks good, but the `routes` are ambiguous when there are multiple nodes of the same class.
195
+ You can work around it by overriding the `identity` method of the node.
196
+
197
+ #### `CallableTree::Node#identity`
198
+
199
+ If you want to customize the node identity, override it.
200
+
201
+ `examples/example3.rb`:
202
+ ```ruby
203
+ module Node
204
+ class Identity
205
+ attr_reader :klass, :type
206
+
207
+ def initialize(klass:, type:)
208
+ @klass = klass
209
+ @type = type
210
+ end
211
+
212
+ def to_s
213
+ "#{klass}(#{type})"
214
+ end
215
+ end
216
+
217
+ module JSON
218
+ ...
219
+
220
+ class Scraper
221
+ include CallableTree::Node::External
222
+
223
+ def initialize(type:)
224
+ @type = type
225
+ end
226
+
227
+ def identity
228
+ Identity.new(klass: super, type: @type)
229
+ end
230
+
231
+ ...
232
+ end
233
+ end
234
+
235
+ module XML
236
+ ...
237
+
238
+ class Scraper
239
+ include CallableTree::Node::External
240
+
241
+ def initialize(type:)
242
+ @type = type
243
+ end
244
+
245
+ def identity
246
+ Identity.new(klass: super, type: @type)
247
+ end
248
+
249
+ ...
250
+ end
251
+ end
252
+ end
253
+
254
+ ...
255
+ ```
256
+
257
+ Run `examples/example3.rb`:
258
+ ```sh
259
+ % ruby examples/example3.rb
260
+ #<struct CallableTree::Node::External::Output value={"Dog"=>"🐶", "Cat"=>"🐱"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007ff1f78e09e0 @klass=Node::JSON::Scraper, @type=:animals>, Node::JSON::Parser, CallableTree::Node::Root]>
261
+ ---
262
+ #<struct CallableTree::Node::External::Output value={"Dog"=>"🐶", "Cat"=>"🐱"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007ff1e7885208 @klass=Node::XML::Scraper, @type=:animals>, Node::XML::Parser, CallableTree::Node::Root]>
263
+ ---
264
+ #<struct CallableTree::Node::External::Output value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007ff1e78771a8 @klass=Node::JSON::Scraper, @type=:fruits>, Node::JSON::Parser, CallableTree::Node::Root]>
265
+ ---
266
+ #<struct CallableTree::Node::External::Output value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007ff1f20d7f50 @klass=Node::XML::Scraper, @type=:fruits>, Node::XML::Parser, CallableTree::Node::Root]>
267
+ ---
268
+ ```
269
+
270
+ #### Logging
271
+
272
+ This is an example of logging.
273
+
274
+ `examples/example4.rb`:
275
+ ```ruby
276
+ module Node
277
+ module Logging
278
+ INDENT_SIZE = 2
279
+ BLANK = ' '.freeze
280
+
281
+ module Match
282
+ LIST_STYLE = '*'.freeze
283
+
284
+ def match?(_input, **)
285
+ super.tap do |matched|
286
+ prefix = LIST_STYLE.rjust(self.depth * INDENT_SIZE - INDENT_SIZE + LIST_STYLE.length, BLANK)
287
+ puts "#{prefix} #{self.identity}: [matched: #{matched}]"
288
+ end
289
+ end
290
+ end
291
+
292
+ module Call
293
+ INPUT_LABEL = 'Input :'.freeze
294
+ OUTPUT_LABEL = 'Output:'.freeze
295
+
296
+ def call(input, **)
297
+ super.tap do |output|
298
+ input_prefix = INPUT_LABEL.rjust(self.depth * INDENT_SIZE + INPUT_LABEL.length, BLANK)
299
+ puts "#{input_prefix} #{input}"
300
+ output_prefix = OUTPUT_LABEL.rjust(self.depth * INDENT_SIZE + OUTPUT_LABEL.length, BLANK)
301
+ puts "#{output_prefix} #{output}"
302
+ end
303
+ end
304
+ end
305
+ end
306
+
307
+ ...
308
+
309
+ module JSON
310
+ class Parser
311
+ include CallableTree::Node::Internal
312
+ prepend Logging::Match
313
+
314
+ ...
315
+ end
316
+
317
+ class Scraper
318
+ include CallableTree::Node::External
319
+ prepend Logging::Match
320
+ prepend Logging::Call
321
+
322
+ ...
323
+ end
324
+ end
325
+
326
+ module XML
327
+ class Parser
328
+ include CallableTree::Node::Internal
329
+ prepend Logging::Match
330
+
331
+ ...
332
+ end
333
+
334
+ class Scraper
335
+ include CallableTree::Node::External
336
+ prepend Logging::Match
337
+ prepend Logging::Call
338
+
339
+ ...
340
+ end
341
+ end
342
+ end
343
+
344
+ ...
345
+ ```
346
+
347
+ Run `examples/example4.rb`:
348
+ ```sh
349
+ % ruby examples/example4.rb
350
+ * Node::JSON::Parser: [matched: true]
351
+ * Node::JSON::Scraper(animals): [matched: true]
352
+ Input : {"animals"=>[{"name"=>"Dog", "emoji"=>"🐶"}, {"name"=>"Cat", "emoji"=>"🐱"}]}
353
+ Output: {"Dog"=>"🐶", "Cat"=>"🐱"}
354
+ #<struct CallableTree::Node::External::Output value={"Dog"=>"🐶", "Cat"=>"🐱"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007f9737074060 @klass=Node::JSON::Scraper, @type=:animals>, Node::JSON::Parser, CallableTree::Node::Root]>
355
+ ---
356
+ * Node::JSON::Parser: [matched: false]
357
+ * Node::XML::Parser: [matched: true]
358
+ * Node::XML::Scraper(animals): [matched: true]
359
+ Input : <root><animals><animal emoji='🐶' name='Dog'/><animal emoji='🐱' name='Cat'/></animals></root>
360
+ Output: {"Dog"=>"🐶", "Cat"=>"🐱"}
361
+ #<struct CallableTree::Node::External::Output value={"Dog"=>"🐶", "Cat"=>"🐱"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007f973710d0f8 @klass=Node::XML::Scraper, @type=:animals>, Node::XML::Parser, CallableTree::Node::Root]>
362
+ ---
363
+ * Node::JSON::Parser: [matched: true]
364
+ * Node::JSON::Scraper(animals): [matched: false]
365
+ * Node::JSON::Scraper(fruits): [matched: true]
366
+ Input : {"fruits"=>[{"name"=>"Red Apple", "emoji"=>"🍎"}, {"name"=>"Green Apple", "emoji"=>"🍏"}]}
367
+ Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
368
+ #<struct CallableTree::Node::External::Output value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007f97370ffed0 @klass=Node::JSON::Scraper, @type=:fruits>, Node::JSON::Parser, CallableTree::Node::Root]>
369
+ ---
370
+ * Node::JSON::Parser: [matched: false]
371
+ * Node::XML::Parser: [matched: true]
372
+ * Node::XML::Scraper(animals): [matched: false]
373
+ * Node::XML::Scraper(fruits): [matched: true]
374
+ Input : <root><fruits><fruit emoji='🍎' name='Red Apple'/><fruit emoji='🍏' name='Green Apple'/></fruits></root>
375
+ Output: {"Red Apple"=>"🍎", "Green Apple"=>"🍏"}
376
+ #<struct CallableTree::Node::External::Output value={"Red Apple"=>"🍎", "Green Apple"=>"🍏"}, options={:foo=>:bar}, routes=[#<Node::Identity:0x00007f97370cceb8 @klass=Node::XML::Scraper, @type=:fruits>, Node::XML::Parser, CallableTree::Node::Root]>
377
+ ---
378
+ ```
379
+
380
+ #### `CallableTree::Node::Hooks::Call` (experimental)
381
+
382
+ `examples/example5.rb`:
383
+ ```ruby
384
+ module Node
385
+ class HooksSample
386
+ include CallableTree::Node::Internal
387
+ prepend CallableTree::Node::Hooks::Call
388
+ end
389
+ end
390
+
391
+ Node::HooksSample.new
392
+ .before_call do |input, **options|
393
+ puts "before_call input: #{input}";
394
+ input + 1
395
+ end
396
+ .append(
397
+ lambda do |input, **options|
398
+ puts "external input: #{input}"
399
+ input * 2
400
+ end
401
+ )
402
+ .around_call do |input, **options, &block|
403
+ puts "around_call input: #{input}"
404
+ output = block.call
405
+ puts "around_call output: #{output}"
406
+ output * input
407
+ end
408
+ .after_call do |output, **options|
409
+ puts "after_call output: #{output}"
410
+ output * 2
411
+ end
412
+ .tap do |tree|
413
+ options = { foo: :bar }
414
+ output = tree.call(1, **options)
415
+ puts "result: #{output}"
416
+ end
417
+ ```
418
+
419
+ Run `examples/example5.rb`:
420
+ ```sh
421
+ % ruby examples/example5.rb
422
+ before_call input: 1
423
+ external input: 2
424
+ around_call input: 2
425
+ around_call output: 4
426
+ after_call output: 8
427
+ result: 16
428
+ ```
429
+
430
+ ## Contributing
431
+
432
+ Bug reports and pull requests are welcome on GitHub at https://github.com/jsmmr/callable_tree.
433
+
434
+ ## License
435
+
436
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).