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.
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'callable_tree'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require "pry"
12
+ # Pry.start
13
+
14
+ require 'irb'
15
+ IRB.start(__FILE__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/callable_tree/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'callable_tree'
7
+ spec.version = CallableTree::VERSION
8
+ spec.authors = ['jsmmr']
9
+ spec.email = ['jsmmr@icloud.com']
10
+
11
+ spec.summary = 'Builds a tree by linking callable nodes. The nodes that match the calling condition are called in a chain from the root node to the leaf node. This is like nested case statements.'
12
+ spec.description = 'Builds a tree by linking callable nodes. The nodes that match the calling condition are called in a chain from the root node to the leaf node. This is like nested case statements.'
13
+ spec.homepage = 'https://github.com/jsmmr/ruby_callable_tree'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = Gem::Requirement.new('>= 2.4.0')
16
+
17
+ spec.metadata['homepage_uri'] = spec.homepage
18
+ spec.metadata['source_code_uri'] = 'https://github.com/jsmmr/ruby_callable_tree'
19
+ spec.metadata['changelog_uri'] = 'https://github.com/jsmmr/ruby_callable_tree/CHANGELOG.md'
20
+
21
+ # Specify which files should be added to the gem when it is released.
22
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
23
+ spec.files = Dir.chdir(File.expand_path(__dir__)) do
24
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{\A(?:test|spec|features)/}) }
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ # spec.add_dependency "example-gem", "~> 1.0"
32
+
33
+ # For more information and examples about making a new gem, checkout our
34
+ # guide at: https://bundler.io/guides/creating_gem.html
35
+ end
@@ -0,0 +1,6 @@
1
+ {
2
+ "animals": [
3
+ { "name": "Dog", "emoji": "🐶" },
4
+ { "name": "Cat", "emoji": "🐱" }
5
+ ]
6
+ }
@@ -0,0 +1 @@
1
+ <root><animals><animal name="Dog" emoji="🐶" /><animal name="Cat" emoji="🐱" /></animals></root>
@@ -0,0 +1,6 @@
1
+ {
2
+ "fruits": [
3
+ { "name": "Red Apple", "emoji": "🍎" },
4
+ { "name": "Green Apple", "emoji": "🍏" }
5
+ ]
6
+ }
@@ -0,0 +1 @@
1
+ <root><fruits><fruit name="Red Apple" emoji="🍎" /><fruit name="Green Apple" emoji="🍏" /></fruits></root>
@@ -0,0 +1,101 @@
1
+ require 'callable_tree'
2
+ require 'json'
3
+ require 'rexml/document'
4
+
5
+ module Node
6
+ module JSON
7
+ class Parser
8
+ include CallableTree::Node::Internal
9
+
10
+ def match?(input, **options)
11
+ File.extname(input) == '.json'
12
+ end
13
+
14
+ def call(input, **options)
15
+ File.open(input) do |file|
16
+ json = ::JSON.load(file)
17
+ super(json, **options)
18
+ end
19
+ end
20
+
21
+ def terminate?(_output, **)
22
+ true
23
+ end
24
+ end
25
+
26
+ class Scraper
27
+ include CallableTree::Node::External
28
+
29
+ def initialize(type:)
30
+ @type = type
31
+ end
32
+
33
+ def match?(input, **options)
34
+ !!input[@type.to_s]
35
+ end
36
+
37
+ def call(input, **options)
38
+ input[@type.to_s]
39
+ .map { |element| [element['name'], element['emoji']] }
40
+ .to_h
41
+ end
42
+ end
43
+ end
44
+
45
+ module XML
46
+ class Parser
47
+ include CallableTree::Node::Internal
48
+
49
+ def match?(input, **options)
50
+ File.extname(input) == '.xml'
51
+ end
52
+
53
+ def call(input, **options)
54
+ File.open(input) do |file|
55
+ super(REXML::Document.new(file), **options)
56
+ end
57
+ end
58
+
59
+ def terminate?(_output, **)
60
+ true
61
+ end
62
+ end
63
+
64
+ class Scraper
65
+ include CallableTree::Node::External
66
+
67
+ def initialize(type:)
68
+ @type = type
69
+ end
70
+
71
+ def match?(input, **options)
72
+ !input.get_elements("//#{@type}").empty?
73
+ end
74
+
75
+ def call(input, **options)
76
+ input
77
+ .get_elements("//#{@type}")
78
+ .first
79
+ .map { |element| [element['name'], element['emoji']] }
80
+ .to_h
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ tree = CallableTree::Node::Root.new.append(
87
+ Node::JSON::Parser.new.append(
88
+ Node::JSON::Scraper.new(type: :animals),
89
+ Node::JSON::Scraper.new(type: :fruits)
90
+ ),
91
+ Node::XML::Parser.new.append(
92
+ Node::XML::Scraper.new(type: :animals),
93
+ Node::XML::Scraper.new(type: :fruits)
94
+ )
95
+ )
96
+
97
+ Dir.glob(__dir__ + '/docs/*') do |file|
98
+ options = { foo: :bar }
99
+ puts tree.call(file, **options)
100
+ puts '---'
101
+ end
@@ -0,0 +1,101 @@
1
+ require 'callable_tree'
2
+ require 'json'
3
+ require 'rexml/document'
4
+
5
+ module Node
6
+ module JSON
7
+ class Parser
8
+ include CallableTree::Node::Internal
9
+
10
+ def match?(input, **options)
11
+ File.extname(input) == '.json'
12
+ end
13
+
14
+ def call(input, **options)
15
+ File.open(input) do |file|
16
+ json = ::JSON.load(file)
17
+ super(json, **options)
18
+ end
19
+ end
20
+
21
+ def terminate?(_output, **)
22
+ true
23
+ end
24
+ end
25
+
26
+ class Scraper
27
+ include CallableTree::Node::External
28
+
29
+ def initialize(type:)
30
+ @type = type
31
+ end
32
+
33
+ def match?(input, **options)
34
+ !!input[@type.to_s]
35
+ end
36
+
37
+ def call(input, **options)
38
+ input[@type.to_s]
39
+ .map { |element| [element['name'], element['emoji']] }
40
+ .to_h
41
+ end
42
+ end
43
+ end
44
+
45
+ module XML
46
+ class Parser
47
+ include CallableTree::Node::Internal
48
+
49
+ def match?(input, **options)
50
+ File.extname(input) == '.xml'
51
+ end
52
+
53
+ def call(input, **options)
54
+ File.open(input) do |file|
55
+ super(REXML::Document.new(file), **options)
56
+ end
57
+ end
58
+
59
+ def terminate?(_output, **)
60
+ true
61
+ end
62
+ end
63
+
64
+ class Scraper
65
+ include CallableTree::Node::External
66
+
67
+ def initialize(type:)
68
+ @type = type
69
+ end
70
+
71
+ def match?(input, **options)
72
+ !input.get_elements("//#{@type}").empty?
73
+ end
74
+
75
+ def call(input, **options)
76
+ input
77
+ .get_elements("//#{@type}")
78
+ .first
79
+ .map { |element| [element['name'], element['emoji']] }
80
+ .to_h
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ tree = CallableTree::Node::Root.new.append(
87
+ Node::JSON::Parser.new.append(
88
+ Node::JSON::Scraper.new(type: :animals).verbosify,
89
+ Node::JSON::Scraper.new(type: :fruits).verbosify
90
+ ),
91
+ Node::XML::Parser.new.append(
92
+ Node::XML::Scraper.new(type: :animals).verbosify,
93
+ Node::XML::Scraper.new(type: :fruits).verbosify
94
+ )
95
+ )
96
+
97
+ Dir.glob(__dir__ + '/docs/*') do |file|
98
+ options = { foo: :bar }
99
+ puts tree.call(file, **options)
100
+ puts '---'
101
+ end
@@ -0,0 +1,122 @@
1
+ require 'callable_tree'
2
+ require 'json'
3
+ require 'rexml/document'
4
+
5
+ module Node
6
+ class Identity
7
+ attr_reader :klass, :type
8
+
9
+ def initialize(klass:, type:)
10
+ @klass = klass
11
+ @type = type
12
+ end
13
+
14
+ def to_s
15
+ "#{klass}(#{type})"
16
+ end
17
+ end
18
+
19
+ module JSON
20
+ class Parser
21
+ include CallableTree::Node::Internal
22
+
23
+ def match?(input, **options)
24
+ File.extname(input) == '.json'
25
+ end
26
+
27
+ def call(input, **options)
28
+ File.open(input) do |file|
29
+ json = ::JSON.load(file)
30
+ super(json, **options)
31
+ end
32
+ end
33
+
34
+ def terminate?(_output, **)
35
+ true
36
+ end
37
+ end
38
+
39
+ class Scraper
40
+ include CallableTree::Node::External
41
+
42
+ def initialize(type:)
43
+ @type = type
44
+ end
45
+
46
+ def identity
47
+ Identity.new(klass: super, type: @type)
48
+ end
49
+
50
+ def match?(input, **options)
51
+ !!input[@type.to_s]
52
+ end
53
+
54
+ def call(input, **options)
55
+ input[@type.to_s]
56
+ .map { |element| [element['name'], element['emoji']] }
57
+ .to_h
58
+ end
59
+ end
60
+ end
61
+
62
+ module XML
63
+ class Parser
64
+ include CallableTree::Node::Internal
65
+
66
+ def match?(input, **options)
67
+ File.extname(input) == '.xml'
68
+ end
69
+
70
+ def call(input, **options)
71
+ File.open(input) do |file|
72
+ super(REXML::Document.new(file), **options)
73
+ end
74
+ end
75
+
76
+ def terminate?(_output, **)
77
+ true
78
+ end
79
+ end
80
+
81
+ class Scraper
82
+ include CallableTree::Node::External
83
+
84
+ def initialize(type:)
85
+ @type = type
86
+ end
87
+
88
+ def identity
89
+ Identity.new(klass: super, type: @type)
90
+ end
91
+
92
+ def match?(input, **options)
93
+ !input.get_elements("//#{@type}").empty?
94
+ end
95
+
96
+ def call(input, **options)
97
+ input
98
+ .get_elements("//#{@type}")
99
+ .first
100
+ .map { |element| [element['name'], element['emoji']] }
101
+ .to_h
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ tree = CallableTree::Node::Root.new.append(
108
+ Node::JSON::Parser.new.append(
109
+ Node::JSON::Scraper.new(type: :animals).verbosify,
110
+ Node::JSON::Scraper.new(type: :fruits).verbosify
111
+ ),
112
+ Node::XML::Parser.new.append(
113
+ Node::XML::Scraper.new(type: :animals).verbosify,
114
+ Node::XML::Scraper.new(type: :fruits).verbosify
115
+ )
116
+ )
117
+
118
+ Dir.glob(__dir__ + '/docs/*') do |file|
119
+ options = { foo: :bar }
120
+ puts tree.call(file, **options)
121
+ puts '---'
122
+ end
@@ -0,0 +1,158 @@
1
+ require 'callable_tree'
2
+ require 'json'
3
+ require 'rexml/document'
4
+
5
+ module Node
6
+ module Logging
7
+ INDENT_SIZE = 2
8
+ BLANK = ' '.freeze
9
+
10
+ module Match
11
+ LIST_STYLE = '*'.freeze
12
+
13
+ def match?(_input, **)
14
+ super.tap do |matched|
15
+ prefix = LIST_STYLE.rjust(self.depth * INDENT_SIZE - INDENT_SIZE + LIST_STYLE.length, BLANK)
16
+ puts "#{prefix} #{self.identity}: [matched: #{matched}]"
17
+ end
18
+ end
19
+ end
20
+
21
+ module Call
22
+ INPUT_LABEL = 'Input :'.freeze
23
+ OUTPUT_LABEL = 'Output:'.freeze
24
+
25
+ def call(input, **)
26
+ super.tap do |output|
27
+ input_prefix = INPUT_LABEL.rjust(self.depth * INDENT_SIZE + INPUT_LABEL.length, BLANK)
28
+ puts "#{input_prefix} #{input}"
29
+ output_prefix = OUTPUT_LABEL.rjust(self.depth * INDENT_SIZE + OUTPUT_LABEL.length, BLANK)
30
+ puts "#{output_prefix} #{output}"
31
+ end
32
+ end
33
+ end
34
+ end
35
+
36
+ class Identity
37
+ attr_reader :klass, :type
38
+
39
+ def initialize(klass:, type:)
40
+ @klass = klass
41
+ @type = type
42
+ end
43
+
44
+ def to_s
45
+ "#{klass}(#{type})"
46
+ end
47
+ end
48
+
49
+ module JSON
50
+ class Parser
51
+ include CallableTree::Node::Internal
52
+ prepend Logging::Match
53
+
54
+ def match?(input, **options)
55
+ File.extname(input) == '.json'
56
+ end
57
+
58
+ def call(input, **options)
59
+ File.open(input) do |file|
60
+ json = ::JSON.load(file)
61
+ super(json, **options)
62
+ end
63
+ end
64
+
65
+ def terminate?(_output, **)
66
+ true
67
+ end
68
+ end
69
+
70
+ class Scraper
71
+ include CallableTree::Node::External
72
+ prepend Logging::Match
73
+ prepend Logging::Call
74
+
75
+ def initialize(type:)
76
+ @type = type
77
+ end
78
+
79
+ def identity
80
+ Identity.new(klass: super, type: @type)
81
+ end
82
+
83
+ def match?(input, **options)
84
+ !!input[@type.to_s]
85
+ end
86
+
87
+ def call(input, **options)
88
+ input[@type.to_s]
89
+ .map { |element| [element['name'], element['emoji']] }
90
+ .to_h
91
+ end
92
+ end
93
+ end
94
+
95
+ module XML
96
+ class Parser
97
+ include CallableTree::Node::Internal
98
+ prepend Logging::Match
99
+
100
+ def match?(input, **options)
101
+ File.extname(input) == '.xml'
102
+ end
103
+
104
+ def call(input, **options)
105
+ File.open(input) do |file|
106
+ super(REXML::Document.new(file), **options)
107
+ end
108
+ end
109
+
110
+ def terminate?(_output, **)
111
+ true
112
+ end
113
+ end
114
+
115
+ class Scraper
116
+ include CallableTree::Node::External
117
+ prepend Logging::Match
118
+ prepend Logging::Call
119
+
120
+ def initialize(type:)
121
+ @type = type
122
+ end
123
+
124
+ def identity
125
+ Identity.new(klass: super, type: @type)
126
+ end
127
+
128
+ def match?(input, **options)
129
+ !input.get_elements("//#{@type}").empty?
130
+ end
131
+
132
+ def call(input, **options)
133
+ input
134
+ .get_elements("//#{@type}")
135
+ .first
136
+ .map { |element| [element['name'], element['emoji']] }
137
+ .to_h
138
+ end
139
+ end
140
+ end
141
+ end
142
+
143
+ tree = CallableTree::Node::Root.new.append(
144
+ Node::JSON::Parser.new.append(
145
+ Node::JSON::Scraper.new(type: :animals).verbosify,
146
+ Node::JSON::Scraper.new(type: :fruits).verbosify
147
+ ),
148
+ Node::XML::Parser.new.append(
149
+ Node::XML::Scraper.new(type: :animals).verbosify,
150
+ Node::XML::Scraper.new(type: :fruits).verbosify
151
+ )
152
+ )
153
+
154
+ Dir.glob(__dir__ + '/docs/*') do |file|
155
+ options = { foo: :bar }
156
+ puts tree.call(file, **options)
157
+ puts '---'
158
+ end