compo 0.3.0 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
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: