tree_branch 1.0.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: 15671327d19ea465e079da408e3e4a35ff0a26d03b8ec33251e7c96a49d9b87d
4
+ data.tar.gz: b40855cf7a130ed233b9e4da84224d81dd75d54de234cabd628d806bda81c9b2
5
+ SHA512:
6
+ metadata.gz: 30b2df3207d1cea66da5197b9582744b8746b4e5fed1f188975f281e8895657bcdf6a96b07bbff9e86b60ff53d4eec492b3d6d4411af77e3258821e2b4d03b72
7
+ data.tar.gz: d6bf7412d4db03c36f2ce842f63705b70af25051d337e70672c23fc95ad442762285a57c956ad839781ddd364d043ea64ace974836381be454785218d138106c
data/.editorconfig ADDED
@@ -0,0 +1,8 @@
1
+ # See http://editorconfig.org/
2
+
3
+ [*]
4
+ trim_trailing_whitespace = true
5
+ indent_style = space
6
+ indent_size = 2
7
+ insert_final_newline = true
8
+ end_of_line = lf
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ .DS_Store
2
+ *.gem
3
+ /tmp
data/.rubocop.yml ADDED
@@ -0,0 +1,8 @@
1
+ Metrics/LineLength:
2
+ Max: 100
3
+
4
+ Metrics/BlockLength:
5
+ ExcludedMethods: ['it', 'describe', 'context', 'let', 'specify']
6
+
7
+ AllCops:
8
+ TargetRubyVersion: 2.3
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.6.0
data/.travis.yml ADDED
@@ -0,0 +1,11 @@
1
+ language: ruby
2
+ rvm:
3
+ # Build on the latest stable of all supported Rubies (https://www.ruby-lang.org/en/downloads/):
4
+ - 2.3.8
5
+ - 2.4.5
6
+ - 2.5.3
7
+ - 2.6.0
8
+ cache: bundler
9
+ script:
10
+ - bundle exec rubocop
11
+ - bundle exec rspec
data/CHANGELOG.md ADDED
File without changes
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,89 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ tree_branch (1.0.0)
5
+ acts_as_hashable (~> 1.0)
6
+
7
+ GEM
8
+ remote: https://rubygems.org/
9
+ specs:
10
+ acts_as_hashable (1.0.3)
11
+ ast (2.4.0)
12
+ coderay (1.1.2)
13
+ diff-lcs (1.3)
14
+ ffi (1.9.25)
15
+ formatador (0.2.5)
16
+ guard (2.15.0)
17
+ formatador (>= 0.2.4)
18
+ listen (>= 2.7, < 4.0)
19
+ lumberjack (>= 1.0.12, < 2.0)
20
+ nenv (~> 0.1)
21
+ notiffany (~> 0.0)
22
+ pry (>= 0.9.12)
23
+ shellany (~> 0.0)
24
+ thor (>= 0.18.1)
25
+ guard-compat (1.2.1)
26
+ guard-rspec (4.7.3)
27
+ guard (~> 2.1)
28
+ guard-compat (~> 1.1)
29
+ rspec (>= 2.99.0, < 4.0)
30
+ jaro_winkler (1.5.1)
31
+ listen (3.1.5)
32
+ rb-fsevent (~> 0.9, >= 0.9.4)
33
+ rb-inotify (~> 0.9, >= 0.9.7)
34
+ ruby_dep (~> 1.2)
35
+ lumberjack (1.0.13)
36
+ method_source (0.9.2)
37
+ nenv (0.3.0)
38
+ notiffany (0.1.1)
39
+ nenv (~> 0.1)
40
+ shellany (~> 0.0)
41
+ parallel (1.12.1)
42
+ parser (2.5.3.0)
43
+ ast (~> 2.4.0)
44
+ powerpack (0.1.2)
45
+ pry (0.12.2)
46
+ coderay (~> 1.1.0)
47
+ method_source (~> 0.9.0)
48
+ rainbow (3.0.0)
49
+ rb-fsevent (0.10.3)
50
+ rb-inotify (0.9.10)
51
+ ffi (>= 0.5.0, < 2)
52
+ rspec (3.8.0)
53
+ rspec-core (~> 3.8.0)
54
+ rspec-expectations (~> 3.8.0)
55
+ rspec-mocks (~> 3.8.0)
56
+ rspec-core (3.8.0)
57
+ rspec-support (~> 3.8.0)
58
+ rspec-expectations (3.8.2)
59
+ diff-lcs (>= 1.2.0, < 2.0)
60
+ rspec-support (~> 3.8.0)
61
+ rspec-mocks (3.8.0)
62
+ diff-lcs (>= 1.2.0, < 2.0)
63
+ rspec-support (~> 3.8.0)
64
+ rspec-support (3.8.0)
65
+ rubocop (0.59.2)
66
+ jaro_winkler (~> 1.5.1)
67
+ parallel (~> 1.10)
68
+ parser (>= 2.5, != 2.5.1.1)
69
+ powerpack (~> 0.1)
70
+ rainbow (>= 2.2.2, < 4.0)
71
+ ruby-progressbar (~> 1.7)
72
+ unicode-display_width (~> 1.0, >= 1.0.1)
73
+ ruby-progressbar (1.10.0)
74
+ ruby_dep (1.5.0)
75
+ shellany (0.0.1)
76
+ thor (0.20.3)
77
+ unicode-display_width (1.4.0)
78
+
79
+ PLATFORMS
80
+ ruby
81
+
82
+ DEPENDENCIES
83
+ guard-rspec (~> 4.7)
84
+ rspec (~> 3.8)
85
+ rubocop (~> 0.59)
86
+ tree_branch!
87
+
88
+ BUNDLED WITH
89
+ 1.17.2
data/Guardfile ADDED
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ require 'guard/rspec/dsl'
5
+ dsl = Guard::RSpec::Dsl.new(self)
6
+
7
+ # RSpec files
8
+ rspec = dsl.rspec
9
+ watch(rspec.spec_helper) { rspec.spec_dir }
10
+ watch(rspec.spec_support) { rspec.spec_dir }
11
+ watch(rspec.spec_files)
12
+
13
+ # Ruby files
14
+ ruby = dsl.ruby
15
+ dsl.watch_spec_files_for(ruby.lib_files)
16
+ end
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2018 Blue Marble Payroll, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,431 @@
1
+ # TreeBranch
2
+
3
+ [![Build Status](https://travis-ci.org/bluemarblepayroll/tree_branch.svg?branch=master)](https://travis-ci.org/bluemarblepayroll/tree_branch)
4
+
5
+ This library allows you to traverse an entire tree structure, compare all nodes, and choose a tree structure to return. The basic input is defined as:
6
+
7
+ 1. Initial Tree structure root node (required)
8
+ 2. Comparison classes or functions (optional)
9
+ 3. Block to convert each matching node (optional)
10
+
11
+ And the output is defined as:
12
+
13
+ 1. Compared and/or converted tree structure (root node)
14
+
15
+ The specific use-case this was designed for was a dynamic web application menu. In this specific example, we wanted either a static file or a database to store and define all possible menus. Then we wanted to input a request's lifecycle context (user, url, parameters, authorization, etc.) and return the menu that matched the current spot in the application.
16
+
17
+ ## Installation
18
+
19
+ To install through Rubygems:
20
+
21
+ ````
22
+ gem install install tree_branch
23
+ ````
24
+
25
+ You can also add this to your Gemfile:
26
+
27
+ ````
28
+ bundle add tree_branch
29
+ ````
30
+
31
+ ## Examples
32
+
33
+ ### Word Processor Application Menu Example
34
+
35
+ Take the following application menu structure:
36
+
37
+ ````ruby
38
+ menu = {
39
+ data: { name: 'Menu' },
40
+ children: [
41
+ {
42
+ data: { name: 'File' },
43
+ children: [
44
+ { data: { name: 'Open', command: :open } },
45
+ { data: { name: 'Save', command: :save, right: :write } },
46
+ { data: { name: 'Close', command: :close } },
47
+ {
48
+ data: { name: 'Print', command: :print },
49
+ children: [
50
+ { data: { name: 'Print' } },
51
+ { data: { name: 'Print Preview' } },
52
+ ]
53
+ },
54
+ ]
55
+ },
56
+ {
57
+ data: { name: 'Edit' },
58
+ children: [
59
+ { data: { name: 'Cut', command: :cut } },
60
+ { data: { name: 'Copy', command: :copy } },
61
+ { data: { name: 'Paste', command: :paste } }
62
+ ]
63
+ }
64
+ ]
65
+ }
66
+ ````
67
+
68
+ There are three application states:
69
+
70
+ 1. No file open (user has no file currently editing): NONE
71
+ 2. Passive file open: PASSIVE
72
+ 3. Active file open: ACTIVE
73
+
74
+ The user is allowed only access to specific menu items depending on their state:
75
+
76
+ 1. NONE: open
77
+ 2. PASSIVE: open, save, close, print
78
+ 3. ACTIVE: open, save, close, print, cut, copy, paste
79
+
80
+ We can implement this as a comparator class:
81
+
82
+ ````ruby
83
+ class StateComparator < ::TreeBranch::Comparator
84
+ STATE_OPS = {
85
+ none: %i[open],
86
+ passive: %i[open save close print],
87
+ active: %i[open save close print cut copy paste]
88
+ }
89
+
90
+ def valid?
91
+ data.command.nil? || Array(STATE_OPS[context.state]).include?(data.command)
92
+ end
93
+ end
94
+ ````
95
+
96
+ Finally, we can process this for all three states:
97
+
98
+ ````ruby
99
+ no_file_menu =
100
+ ::TreeBranch.process(
101
+ node: menu,
102
+ comparators: StateComparator,
103
+ context: { state: :none }
104
+ )
105
+
106
+ passive_file_menu =
107
+ ::TreeBranch.process(
108
+ node: menu,
109
+ comparators: StateComparator,
110
+ context: { state: :passive }
111
+ )
112
+
113
+ active_file_menu =
114
+ ::TreeBranch.process(
115
+ node: menu,
116
+ comparators: StateComparator,
117
+ context: { state: :active }
118
+ )
119
+ ````
120
+
121
+ We would get the following structure back (in the form of a root Node object but expressed as a hash below):
122
+
123
+ ##### No File Menu Result
124
+
125
+ ````ruby
126
+ {
127
+ data: { name: 'Menu' },
128
+ children: [
129
+ {
130
+ data: { name: 'File' },
131
+ children: [
132
+ { data: { name: 'Open', command: :open } }
133
+ ]
134
+ },
135
+ {
136
+ data: { name: 'Edit' }
137
+ }
138
+ ]
139
+ }
140
+ ````
141
+
142
+ ##### Passive File Menu Result
143
+
144
+ ````ruby
145
+ {
146
+ data: { name: 'Menu' },
147
+ children: [
148
+ {
149
+ data: { name: 'File' },
150
+ children: [
151
+ { data: { name: 'Open', command: :open } },
152
+ { data: { name: 'Save', command: :save, right: :write } },
153
+ { data: { name: 'Close', command: :close } },
154
+ {
155
+ data: { name: 'Print', command: :print },
156
+ children: [
157
+ { data: { name: 'Print' } },
158
+ { data: { name: 'Print Preview' } }
159
+ ]
160
+ }
161
+ ]
162
+ },
163
+ {
164
+ data: { name: 'Edit' }
165
+ }
166
+ ]
167
+ }
168
+ ````
169
+
170
+ ##### Active File Menu Result
171
+
172
+ ````ruby
173
+ {
174
+ data: { name: 'Menu' },
175
+ children: [
176
+ {
177
+ data: { name: 'File' },
178
+ children: [
179
+ { data: { name: 'Open', command: :open } },
180
+ { data: { name: 'Save', command: :save, right: :write } },
181
+ { data: { name: 'Close', command: :close } },
182
+ {
183
+ data: { name: 'Print', command: :print },
184
+ children: [
185
+ { data: { name: 'Print' } },
186
+ { data: { name: 'Print Preview' } },
187
+ ]
188
+ },
189
+ ]
190
+ },
191
+ {
192
+ data: { name: 'Edit' },
193
+ children: [
194
+ { data: { name: 'Cut', command: :cut } },
195
+ { data: { name: 'Copy', command: :copy } },
196
+ { data: { name: 'Paste', command: :paste } }
197
+ ]
198
+ }
199
+ ]
200
+ }
201
+ ````
202
+
203
+ ### Stacking Comparators
204
+
205
+ You can also choose to input multiple comparators (technically 0 to N). For example, let's stack authorization into our application menu example using this comparator:
206
+
207
+ ````ruby
208
+ class AuthorizationComparator < ::TreeBranch::Comparator
209
+ def valid?
210
+ data.right.nil? || Array(context.rights).include?(data.right)
211
+ end
212
+ end
213
+ ````
214
+
215
+ Now, we can pass in our current user's rights and use them when appropriate:
216
+
217
+ ````ruby
218
+ passive_read_only_menu =
219
+ ::TreeBranch.process(
220
+ node: menu,
221
+ comparators: [StateComparator, AuthorizationComparator],
222
+ context: { state: :passive }
223
+ )
224
+
225
+ passive_read_write_menu =
226
+ ::TreeBranch.process(
227
+ node: menu,
228
+ comparators: [StateComparator, AuthorizationComparator],
229
+ context: { state: :passive, rights: :write }
230
+ )
231
+ ````
232
+
233
+ ##### Read-Only User Passively Editing Result
234
+
235
+ ````ruby
236
+ {
237
+ data: { name: 'Menu' },
238
+ children: [
239
+ {
240
+ data: { name: 'File' },
241
+ children: [
242
+ { data: { name: 'Open', command: :open } },
243
+ { data: { name: 'Close', command: :close } },
244
+ {
245
+ data: { name: 'Print', command: :print },
246
+ children: [
247
+ { data: { name: 'Print' } },
248
+ { data: { name: 'Print Preview' } }
249
+ ]
250
+ }
251
+ ]
252
+ },
253
+ {
254
+ data: { name: 'Edit' }
255
+ }
256
+ ]
257
+ }
258
+ ````
259
+
260
+ ##### Read/Write User Passively Editing Result
261
+
262
+ ````ruby
263
+ {
264
+ data: { name: 'Menu' },
265
+ children: [
266
+ {
267
+ data: { name: 'File' },
268
+ children: [
269
+ { data: { name: 'Open', command: :open } },
270
+ { data: { name: 'Save', command: :save, right: :write } },
271
+ { data: { name: 'Close', command: :close } },
272
+ {
273
+ data: { name: 'Print', command: :print },
274
+ children: [
275
+ { data: { name: 'Print' } },
276
+ { data: { name: 'Print Preview' } }
277
+ ]
278
+ }
279
+ ]
280
+ },
281
+ {
282
+ data: { name: 'Edit' }
283
+ }
284
+ ]
285
+ }
286
+ ````
287
+
288
+ Notice now our read-only menu is missing the 'save' item.
289
+
290
+ ### Comparator Creation
291
+
292
+ There are two ways to create comparators:
293
+
294
+ 1. Subclass ::TreeBranch::Comparator and implement the ```valid?``` method to return true/false
295
+ 2. Create lambda/proc that accepts two arguments: data and context and returns true/false
296
+
297
+ Option one is shown in the above example, while option two can be illustrated as:
298
+
299
+ ````ruby
300
+ auth_comparator = lambda do |data, context|
301
+ data.right.nil? || Array(context.rights).include?(data.right)
302
+ end
303
+
304
+ passive_read_only_menu =
305
+ ::TreeBranch.process(
306
+ node: menu,
307
+ comparators: [StateComparator, auth_comparator],
308
+ context: { state: :passive }
309
+ )
310
+ ````
311
+
312
+ ### Node Post-Processing / Conversion
313
+
314
+ After a node has been compared and is deemed to be valid, it will either return one of two things:
315
+
316
+ 1. ::TreeBranch::Node instance
317
+ 2. The return of the block passed into the process method produced *(Note: If nil it will be ignored as if it was invalid.)*
318
+
319
+ In our above example we did not pass in a block so they would all return Node instances. The passed in block is your chance to return instances of another class, or even do some other post-processing routines. For example, lets return an instance of a new type: MenuItem as shown below:
320
+
321
+ ````ruby
322
+ class MenuItem
323
+ acts_as_hashable
324
+
325
+ attr_reader :menu_items, :name
326
+
327
+ def initialize(name: '', menu_items: [])
328
+ @name = name
329
+ @menu_items = self.class.array(menu_items)
330
+ end
331
+
332
+ def eql?(other)
333
+ name == other.name && menu_items == other.menu_items
334
+ end
335
+
336
+ def ==(other)
337
+ eql?(other)
338
+ end
339
+ end
340
+ ````
341
+
342
+ We can now convert this in the block:
343
+
344
+ ````ruby
345
+ passive_read_write_menu =
346
+ ::TreeBranch.process(
347
+ node: menu,
348
+ comparators: [StateComparator, auth_comparator],
349
+ context: { state: :passive, rights: :write }
350
+ ) { |data, children, context| MenuItem.new(data.name, children) }
351
+ ````
352
+
353
+ Our resulting data set (visualized as a hash):
354
+
355
+ ````ruby
356
+ {
357
+ name: 'Menu',
358
+ menu_items: [
359
+ {
360
+ name: 'File',
361
+ menu_items: [
362
+ { name: 'Open' },
363
+ { name: 'Save' },
364
+ { name: 'Close' },
365
+ {
366
+ name: 'Print',
367
+ menu_items: [
368
+ { name: 'Print' },
369
+ { name: 'Print Preview' }
370
+ ]
371
+ }
372
+ ]
373
+ },
374
+ {
375
+ name: 'Edit'
376
+ }
377
+ ]
378
+ }
379
+ ````
380
+
381
+ ## Contributing
382
+
383
+ ### Development Environment Configuration
384
+
385
+ Basic steps to take to get this repository compiling:
386
+
387
+ 1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (check tree_branch.gemspec for versions supported)
388
+ 2. Install bundler (gem install bundler)
389
+ 3. Clone the repository (git clone git@github.com:bluemarblepayroll/tree_branch.git)
390
+ 4. Navigate to the root folder (cd tree_branch)
391
+ 5. Install dependencies (bundle)
392
+
393
+ ### Running Tests
394
+
395
+ To execute the test suite run:
396
+
397
+ ````
398
+ bundle exec rspec spec --format documentation
399
+ ````
400
+
401
+ Alternatively, you can have Guard watch for changes:
402
+
403
+ ````
404
+ bundle exec guard
405
+ ````
406
+
407
+ Also, do not forget to run Rubocop:
408
+
409
+ ````
410
+ bundle exec rubocop
411
+ ````
412
+
413
+ ### Publishing
414
+
415
+ Note: ensure you have proper authorization before trying to publish new versions.
416
+
417
+ After code changes have successfully gone through the Pull Request review process then the following steps should be followed for publishing new versions:
418
+
419
+ 1. Merge Pull Request into master
420
+ 2. Update [lib/tree_branch/version.rb](https://github.com/bluemarblepayroll/tree_branch/blob/master/lib/tree_branch/version.rb) [version number](https://semver.org/)
421
+ 3. Bundle
422
+ 4. Update CHANGELOG.md
423
+ 5. Commit & Push master to remote and ensure CI builds master successfully
424
+ 6. Build the project locally: `gem build tree_branch`
425
+ 7. Publish package to NPM: `gem push tree_branch-X.gem` where X is the version to push
426
+ 8. Tag master with new version: `git tag <version>`
427
+ 9. Push tags remotely: `git push origin --tags`
428
+
429
+ ## License
430
+
431
+ This project is MIT Licensed.
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 'tree_branch'
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
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module TreeBranch
11
+ # This is the base class for all plug in comparators. Derive subclasses from this class
12
+ # and declare them when calling ::TreeBranch::Node#process or ::TreeBranch#process.
13
+ class Comparator
14
+ attr_reader :data, :context
15
+
16
+ def initialize(data: {}, context: {})
17
+ @data = data || {}
18
+ @context = context || {}
19
+ end
20
+
21
+ def valid?
22
+ false
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module TreeBranch
11
+ # Main class the outlines the basic operations and structure of a node in the tree.
12
+ class Node
13
+ attr_reader :data, :children
14
+
15
+ def initialize(data)
16
+ @data = data
17
+ @children = []
18
+ end
19
+
20
+ def add(*children_to_add)
21
+ children_to_add.flatten.each do |child|
22
+ raise ArgumentError, "Improper class: #{child.class.name}" unless child.is_a?(self.class)
23
+
24
+ @children << child
25
+ end
26
+
27
+ self
28
+ end
29
+
30
+ def eql?(other)
31
+ data == other.data && children == other.children
32
+ end
33
+
34
+ def ==(other)
35
+ eql?(other)
36
+ end
37
+
38
+ def to_s
39
+ "[#{self.class.name}] Data: #{data}, Child Count: #{children.length}"
40
+ end
41
+ end
42
+ end