compo 0.3.0 → 0.3.1

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
  SHA1:
3
- metadata.gz: de0ae1a9f06c21a0818cb86865d5bd26d79aa7e0
4
- data.tar.gz: 7cce675b54964af406c7391913fde1b8f6741c34
3
+ metadata.gz: 463952c33e420e5ab401ec9774208262b4606ebc
4
+ data.tar.gz: 59ff0efef46f9db2fd9fdf72c463fccb46b18b96
5
5
  SHA512:
6
- metadata.gz: 075f6b9688444e1ee13333c97fddf7fce91d897ac4088df6e56fa9c1a5cce5e75b2a279ed2ecbc89999d72da57e95d380f830490a847c7a288586ef06b32abce
7
- data.tar.gz: 6900ffec47c87b03f12de21196b2a603b54569882a5e20ee682e5eca555740d2e33ee887f102142f447406ccd6a32dea89e3fe253a9057178ac4bcac91d3a0b9
6
+ metadata.gz: ead1e453e9ba923f8dcb12b2c9ec211d8d8946323557175eb82c0236939afc42314bc83c139f4e4076b1d3c0f4850910afcad3a29ed4b67ef9309e209eb97dbc
7
+ data.tar.gz: 890b61d951ad12e56aa2f329506f803dbde105c8541afd2332fb520f973a745fbacd845a7b1f213f69230cfa51e4c326c9704427561a78c754fd6ed69ee1d14c
data/CHANGELOG CHANGED
@@ -1,3 +1,8 @@
1
+ 0.3.1 (2014-05-21)
2
+ - Add UrlFinder class, for finding children inside a composite structure via
3
+ their URLs.
4
+ - 100% YARD, LOC and Rubocop coverage.
5
+
1
6
  0.3.0 (2014-05-21)
2
7
  - Require bundler 1.6.
3
8
  - Remove some unnecessary checks for nil: Parentless should be used instead.
data/README.md CHANGED
@@ -3,11 +3,11 @@
3
3
  **Compo** is a library providing mixins and base classes for setting up
4
4
  composite objects.
5
5
 
6
- It implements something similar to the Gang of Four Composite pattern, but with
7
- the difference that children are identified in their parents by an *ID*,
8
- such as the index or hash key, that the child is aware of at all times.
6
+ It's similar to the Gang of Four Composite pattern, but in Compo
7
+ children are identified in their parents by an *ID*,
8
+ such as an index or hash key, that the child is aware of at all times.
9
9
 
10
- Compo was designed for the purpose of creating models whose natural composite
10
+ Compo was designed for models whose composite
11
11
  structure can be expressed as URLs made from their recursive ID trails.
12
12
 
13
13
  ## Installation
@@ -33,6 +33,8 @@ Compo consists of several classes and mixins that implement various parts of the
33
33
  - **Composite**, which specifies the #add/#remove/#remove_id/#get_child API on top of an implementation;
34
34
  - **Movable**, which creates a #move_to method that simplifies the act of moving a child between Composites;
35
35
  - **ParentTracker**, which implements keeping track of a child's parent and the child end of #add/#remove;
36
+ - **URLReferenceable**, which allows the current position of an object in a hierarchy of composites to be retrieved as an URL-style reference;
37
+ - **Branch**, which is simply a **Moveable** **URLReferenceable** **ParentTracker**.
36
38
 
37
39
  ### Composite implementation classes
38
40
 
@@ -44,9 +46,13 @@ These implement the methods needed for **Composite** to work, but don't implemen
44
46
 
45
47
  ### Simple leaf and node classes
46
48
 
47
- These include **Composite**, **Movable** and **ParentTracker**, and thus can be moved around, added to and removed from composites, and track their current parents and IDs.
49
+ These include **Branch**, and thus can be moved around, added to and removed from composites, and track their current parents and IDs.
48
50
 
49
- - **Leaf**, which is based on NullComposite and is intended for objects that don't have children but can be placed in other Composites.
51
+ - **Leaf**, which is based on NullComposite and is intended for objects that don't have children but can be placed in other Composites;
52
+ - **ArrayBranch**, which is based on ArrayComposite;
53
+ - **HashBranch**, which is based on HashComposite.
54
+
55
+ Generally, you'll only need to use these classes (use **Leaf** when creating objects that can be part of other objects but not have children themselves, **ArrayBranch** when an object has a list of ordered children *whose ordering changes as more children are added and deleted*, and **HashBranch** when it has a collection of children with arbitrary IDs).
50
56
 
51
57
  ## Contributing
52
58
 
data/lib/compo.rb CHANGED
@@ -15,6 +15,9 @@ require 'compo/array_branch'
15
15
  require 'compo/hash_branch'
16
16
  require 'compo/leaf'
17
17
 
18
+ # Utilities
19
+ require 'compo/url_finder'
20
+
18
21
  # Misc
19
22
  require 'compo/version'
20
23
 
@@ -73,6 +73,9 @@ module Compo
73
73
 
74
74
  # Gets the child in this Composite with the given ID
75
75
  #
76
+ # The ID is compared directly against the IDs of the children of this
77
+ # composite. To use a predicate to find an ID, use #get_child_such_that.
78
+ #
76
79
  # @api public
77
80
  # @example Gets the child with ID :in, if children is {in: 3}.
78
81
  # composite.get_child(:in)
@@ -80,6 +83,9 @@ module Compo
80
83
  # @example Fails to get the child with ID :out, if children is {in: 3}.
81
84
  # composite.get_child(:out)
82
85
  # #=> nil
86
+ # @example Fails to get the child with ID '1', if children is {1 => 3}.
87
+ # composite.get_child('1')
88
+ # #=> nil
83
89
  #
84
90
  # @param id [Object] The ID of the child to get from this Composite.
85
91
  #
@@ -88,6 +94,32 @@ module Compo
88
94
  children[id]
89
95
  end
90
96
 
97
+ # Gets the child in this Composite whose ID matches a given predicate
98
+ #
99
+ # If multiple children match this predicate, the result is the first child
100
+ # in the hash.
101
+ #
102
+ # @api public
103
+ # @example Gets the child with ID :in, if children is {in: 3}.
104
+ # composite.get_child_such_that { |x| x == :in }
105
+ # #=> 3
106
+ # @example Fails to get the child with ID :out, if children is {in: 3}.
107
+ # composite.get_child_such_that { |x| x == :out }
108
+ # #=> nil
109
+ # @example Get the child with an ID whose string form is '1', if children
110
+ # is {1 => 3}.
111
+ # composite.get_child_such_that { |x| x.to_s == '3' }
112
+ # #=> 3
113
+ #
114
+ # @yieldparam id [Object] An ID to check against the predicate.
115
+ #
116
+ # @return [Object] The child if successful; nil otherwise.
117
+ def get_child_such_that(&block)
118
+ child = children.each.find { |k, _| block.call(k) }
119
+ (_, value) = child unless child.nil?
120
+ value
121
+ end
122
+
91
123
  def_delegator :children, :each
92
124
 
93
125
  protected
@@ -10,6 +10,16 @@ module Compo
10
10
  module ParentTracker
11
11
  extend Forwardable
12
12
 
13
+ # Initialises the ParentTracker
14
+ #
15
+ # This constructor sets the tracker up so it initially has an instance of
16
+ # Parentless as its 'parent'.
17
+ #
18
+ # @api semipublic
19
+ # @example Creates a new ParentTracker.
20
+ # ParentTracker.new
21
+ #
22
+ # @return [Void]
13
23
  def initialize
14
24
  super()
15
25
  remove_parent
@@ -0,0 +1,164 @@
1
+ module Compo
2
+ # An 'UrlFinder' finds an object in a composite tree via its URL
3
+ #
4
+ # It is a method object that implements only the finding of a specific URL.
5
+ # UrlFinders are *not* thread-safe.
6
+ class UrlFinder
7
+ # Initialises a UrlFinder
8
+ #
9
+ # @api public
10
+ # @example Initialises an UrlFinder with default missing resource handling.
11
+ # UrlFinder.new(composite, 'a/b/c')
12
+ # @example Initialises an UrlFinder returning a default value.
13
+ # UrlFinder.new(composite, 'a/b/c', missing_proc=->(_) { :default })
14
+ #
15
+ # @param root [Composite] A composite object serving as the root of the
16
+ # search tree, and the URL.
17
+ #
18
+ # @param url [String] A partial URL that follows this model object's URL
19
+ # to form the URL of the resource to locate. Can be nil, in which case
20
+ # this object is returned.
21
+ #
22
+ # @param missing_proc [Proc] A proc to call, with the requested URL, if the
23
+ # resource could not be found. If nil (the default), this raises a string
24
+ # exception.
25
+ def initialize(root, url, missing_proc: nil)
26
+ @root = root
27
+ @url = url
28
+ @missing_proc = missing_proc || method(:default_missing_proc)
29
+
30
+ reset
31
+ end
32
+
33
+ # Finds the model object at a URL, given a model root
34
+ #
35
+ # @api public
36
+ # @example Finds a URL with default missing resource handling.
37
+ # UrlFinder.find(composite, 'a/b/c') { |item| p item }
38
+ #
39
+ # @param (see #initialize)
40
+ #
41
+ # @yieldparam (see #run)
42
+ #
43
+ # @return [Object] The return value of the block.
44
+ def self.find(*args, &block)
45
+ new(*args).run(&block)
46
+ end
47
+
48
+ # Attempts to find a child resource with the given partial URL
49
+ #
50
+ # If the resource is found, it will be yielded to the attached block;
51
+ # otherwise, an exception will be raised.
52
+ #
53
+ # @api public
54
+ # @example Runs an UrlFinder, returning the item unchanged.
55
+ # finder.run { |item| item }
56
+ # #=> item
57
+ #
58
+ # @yieldparam resource [ModelObject] The resource found.
59
+ # @yieldparam args [Array] The splat from above.
60
+ #
61
+ # @return [Object] The return value of the block.
62
+ def run
63
+ # We're traversing down the URL by repeatedly splitting it into its
64
+ # head (part before the next /) and tail (part after). While we still
65
+ # have a tail, then the URL still needs walking down.
66
+ reset
67
+ descend until hit_end_of_url?
68
+ yield @resource
69
+ end
70
+
71
+ private
72
+
73
+ # Performs a descending step in the URL finder
74
+ #
75
+ # This tries to move down a level of the URL hierarchy, fetches the
76
+ # resource at that level, and fails according to @missing_proc if there is
77
+ # no such resource.
78
+ #
79
+ # @api private
80
+ #
81
+ # @return [Void]
82
+ def descend
83
+ descend_url
84
+ next_resource
85
+ fail_with_no_resource if @resource.nil?
86
+ end
87
+
88
+ # Seeks to the next resource pointed at by @next_id
89
+ #
90
+ # @api private
91
+ #
92
+ # @return [Void]
93
+ def next_resource
94
+ @resource = @resource.get_child_such_that { |id| id.to_s == @next_id }
95
+ end
96
+
97
+ # Fails, using @missing_proc, due to a missing resource
98
+ #
99
+ # @api private
100
+ #
101
+ # @return [Void]
102
+ def fail_with_no_resource
103
+ # If the proc returns a value instead of raising an error, then set
104
+ # things up so that value is yielded in place of the missing resource.
105
+ @tail = nil
106
+ @resource = @missing_proc.call(@url)
107
+ end
108
+
109
+ # Default value for @missing_proc
110
+ #
111
+ # @api private
112
+ #
113
+ # @param url [String] The URL whose finding failed.
114
+ #
115
+ # @return [Void]
116
+ def default_missing_proc(url)
117
+ fail("Could not find resource: #{url}")
118
+ end
119
+
120
+ # Decides whether we have reached the end of the URL
121
+ #
122
+ # @api private
123
+ #
124
+ # @return [Boolean] Whether we have hit the end of the URL.
125
+ def hit_end_of_url?
126
+ @tail.nil? || @tail.empty?
127
+ end
128
+
129
+ # Splits the tail on the next URL level
130
+ #
131
+ # @api private
132
+ #
133
+ # @return [Void]
134
+ def descend_url
135
+ @next_id, @tail = @tail.split('/', 2)
136
+ end
137
+
138
+ # Resets this UrlFinder so it can be used again
139
+ #
140
+ # @api private
141
+ #
142
+ # @return [Void]
143
+ def reset
144
+ @next_id, @tail = nil, trimmed_url
145
+ @resource = @root
146
+ end
147
+
148
+ # Removes any leading or trailing slash from the URL, returning the result
149
+ #
150
+ # This only removes one leading or trailing slash. Thus, '///' will be
151
+ # returned as '/'.
152
+ #
153
+ # @api private
154
+ #
155
+ # @return [String] The URL with no trailing or leading slash.
156
+ def trimmed_url
157
+ first, last = 0, @url.length
158
+ first += 1 if @url.start_with?('/')
159
+ last -= 1 if @url.end_with?('/')
160
+
161
+ @url[first...last]
162
+ end
163
+ end
164
+ end
data/lib/compo/version.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  # The current gem version. See CHANGELOG for information.
2
2
  module Compo
3
- VERSION = '0.3.0'
3
+ VERSION = '0.3.1'
4
4
  end
@@ -16,12 +16,12 @@ shared_examples 'a branch' do
16
16
  end
17
17
 
18
18
  describe '#url' do
19
- context 'when the Leaf has no parent' do
19
+ context 'when the Branch has no parent' do
20
20
  it 'returns the empty string' do
21
21
  expect(subject.url).to eq('')
22
22
  end
23
23
  end
24
- context 'when the Leaf is the child of a root' do
24
+ context 'when the Branch is the child of a root' do
25
25
  let(:parent) { Compo::HashBranch.new }
26
26
  before(:each) { subject.move_to(parent, :id) }
27
27
 
@@ -30,4 +30,22 @@ shared_examples 'a branch' do
30
30
  end
31
31
  end
32
32
  end
33
+
34
+ describe '#move_to' do
35
+ context 'when the Branch has a parent' do
36
+ context 'when the new parent is nil' do
37
+ let(:parent) { Compo::HashBranch.new }
38
+ before(:each) { subject.move_to(parent, :id) }
39
+
40
+ it 'loses its previous parent' do
41
+ expect(subject.move_to(nil, :id).parent).to be_a(Compo::Parentless)
42
+ end
43
+
44
+ it 'is no longer a child of its parent' do
45
+ subject.move_to(nil, :id)
46
+ expect(parent.children).to_not include(subject)
47
+ end
48
+ end
49
+ end
50
+ end
33
51
  end
@@ -0,0 +1,12 @@
1
+ require 'spec_helper'
2
+ require 'compo'
3
+ require 'branch_shared_examples'
4
+
5
+ # Mock implementation of a Branch
6
+ class MockBranch
7
+ include Compo::Branch
8
+ end
9
+
10
+ describe MockBranch do
11
+ it_behaves_like 'a branch'
12
+ end
@@ -5,6 +5,9 @@ require 'compo'
5
5
  class MockParentTracker
6
6
  include Compo::ParentTracker
7
7
 
8
+ # Initialises a MockParentTracker
9
+ #
10
+ # @api private
8
11
  def initialize(parent, id_function)
9
12
  @parent = parent
10
13
  @id_function = id_function
@@ -0,0 +1,129 @@
1
+ require 'spec_helper'
2
+ require 'compo'
3
+
4
+ describe Compo::UrlFinder do
5
+ subject { Compo::UrlFinder }
6
+ describe '.find' do
7
+ let(:target) { Compo::Leaf.new }
8
+
9
+ let(:root) do
10
+ s = Compo::HashBranch.new
11
+ a = Compo::HashBranch.new
12
+ b = Compo::ArrayBranch.new
13
+ d = Compo::Leaf.new
14
+ e = Compo::Leaf.new
15
+ zero = Compo::Leaf.new
16
+
17
+ s.add('a', a)
18
+ a.add('b', b)
19
+ b.add(0, zero)
20
+ b.add(1, target)
21
+ s.add('d', d)
22
+ a.add('e', e)
23
+ s
24
+ end
25
+
26
+ context 'when given a nil root' do
27
+ specify { expect { |b| subject.find(nil, 'a/b/1', &b) }.to raise_error }
28
+ end
29
+
30
+ context 'when given a nil URL' do
31
+ specify { expect { |b| subject.find(root, nil, &b) }.to raise_error }
32
+ end
33
+
34
+ shared_examples 'a successful finding' do
35
+ it 'returns the correct resource' do
36
+ expect { |b| subject.find(root, url, &b) }.to yield_with_args(target)
37
+ end
38
+ end
39
+
40
+ shared_examples 'an unsuccessful finding' do
41
+ specify do
42
+ expect { |b| subject.find(root, url, &b) }.to raise_error(
43
+ "Could not find resource: #{url}"
44
+ )
45
+ end
46
+ end
47
+
48
+ shared_examples 'an unsuccessful finding with custom error' do
49
+ specify do
50
+ mp = ->(_) { :a }
51
+ expect { |b| subject.find(root, url, missing_proc: mp, &b) }
52
+ .to yield_with_args(:a)
53
+ end
54
+ end
55
+
56
+ context 'when given a correct root but incorrect URL' do
57
+ context 'using the default missing resource handler' do
58
+ context 'when the URL has a leading slash' do
59
+ it_behaves_like 'an unsuccessful finding' do
60
+ let(:url) { '/a/z' }
61
+ end
62
+ end
63
+ context 'when the URL has a trailing slash' do
64
+ it_behaves_like 'an unsuccessful finding' do
65
+ let(:url) { 'a/z/' }
66
+ end
67
+ end
68
+ context 'when the URL has a leading and trailing slash' do
69
+ it_behaves_like 'an unsuccessful finding' do
70
+ let(:url) { '/a/z/' }
71
+ end
72
+ end
73
+ context 'when the URL has neither leading nor trailing slash' do
74
+ it_behaves_like 'an unsuccessful finding' do
75
+ let(:url) { 'a/z' }
76
+ end
77
+ end
78
+ end
79
+
80
+ context 'using a custom error handler' do
81
+ context 'when the URL has a leading slash' do
82
+ it_behaves_like 'an unsuccessful finding with custom error' do
83
+ let(:url) { '/d/e/d' }
84
+ end
85
+ end
86
+ context 'when the URL has a trailing slash' do
87
+ it_behaves_like 'an unsuccessful finding with custom error' do
88
+ let(:url) { 'd/e/d/' }
89
+ end
90
+ end
91
+ context 'when the URL has a leading and trailing slash' do
92
+ it_behaves_like 'an unsuccessful finding with custom error' do
93
+ let(:url) { '/d/e/d/' }
94
+ end
95
+ end
96
+ context 'when the URL has neither leading nor trailing slash' do
97
+ it_behaves_like 'an unsuccessful finding with custom error' do
98
+ let(:url) { 'd/e/d' }
99
+ end
100
+ end
101
+ end
102
+ end
103
+
104
+ context 'when given a correct root and URL' do
105
+ context 'when the URL leads to a resource' do
106
+ context 'when the URL has a leading slash' do
107
+ it_behaves_like 'a successful finding' do
108
+ let(:url) { '/a/b/1' }
109
+ end
110
+ end
111
+ context 'when the URL has a trailing slash' do
112
+ it_behaves_like 'a successful finding' do
113
+ let(:url) { 'a/b/1/' }
114
+ end
115
+ end
116
+ context 'when the URL has a leading and trailing slash' do
117
+ it_behaves_like 'a successful finding' do
118
+ let(:url) { '/a/b/1/' }
119
+ end
120
+ end
121
+ context 'when the URL has neither leading nor trailing slash' do
122
+ it_behaves_like 'a successful finding' do
123
+ let(:url) { 'a/b/1' }
124
+ end
125
+ end
126
+ end
127
+ end
128
+ end
129
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: compo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Matt Windsor
@@ -151,12 +151,14 @@ files:
151
151
  - lib/compo/null_composite.rb
152
152
  - lib/compo/parent_tracker.rb
153
153
  - lib/compo/parentless.rb
154
+ - lib/compo/url_finder.rb
154
155
  - lib/compo/url_referenceable.rb
155
156
  - lib/compo/version.rb
156
157
  - spec/array_branch_spec.rb
157
158
  - spec/array_composite_shared_examples.rb
158
159
  - spec/array_composite_spec.rb
159
160
  - spec/branch_shared_examples.rb
161
+ - spec/branch_spec.rb
160
162
  - spec/composite_shared_examples.rb
161
163
  - spec/composite_spec.rb
162
164
  - spec/hash_branch_spec.rb
@@ -170,6 +172,7 @@ files:
170
172
  - spec/parent_tracker_spec.rb
171
173
  - spec/parentless_spec.rb
172
174
  - spec/spec_helper.rb
175
+ - spec/url_finder_spec.rb
173
176
  - spec/url_referenceable_shared_examples.rb
174
177
  - spec/url_referenceable_spec.rb
175
178
  homepage: http://github.com/CaptainHayashi/compo
@@ -201,6 +204,7 @@ test_files:
201
204
  - spec/array_composite_shared_examples.rb
202
205
  - spec/array_composite_spec.rb
203
206
  - spec/branch_shared_examples.rb
207
+ - spec/branch_spec.rb
204
208
  - spec/composite_shared_examples.rb
205
209
  - spec/composite_spec.rb
206
210
  - spec/hash_branch_spec.rb
@@ -214,6 +218,7 @@ test_files:
214
218
  - spec/parent_tracker_spec.rb
215
219
  - spec/parentless_spec.rb
216
220
  - spec/spec_helper.rb
221
+ - spec/url_finder_spec.rb
217
222
  - spec/url_referenceable_shared_examples.rb
218
223
  - spec/url_referenceable_spec.rb
219
224
  has_rdoc: