callable_tree 0.1.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 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).