compo 0.3.1 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (59) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG +8 -0
  3. data/lib/compo/branches/array.rb +17 -0
  4. data/lib/compo/branches/branch.rb +35 -0
  5. data/lib/compo/branches/constant.rb +34 -0
  6. data/lib/compo/branches/hash.rb +17 -0
  7. data/lib/compo/branches/leaf.rb +15 -0
  8. data/lib/compo/branches.rb +15 -0
  9. data/lib/compo/composites/array.rb +105 -0
  10. data/lib/compo/composites/composite.rb +183 -0
  11. data/lib/compo/composites/hash.rb +72 -0
  12. data/lib/compo/composites/leaf.rb +64 -0
  13. data/lib/compo/composites/parentless.rb +133 -0
  14. data/lib/compo/composites.rb +20 -0
  15. data/lib/compo/finders/url.rb +166 -0
  16. data/lib/compo/finders.rb +10 -0
  17. data/lib/compo/mixins/movable.rb +55 -0
  18. data/lib/compo/mixins/parent_tracker.rb +74 -0
  19. data/lib/compo/mixins/url_referenceable.rb +70 -0
  20. data/lib/compo/mixins.rb +9 -0
  21. data/lib/compo/version.rb +1 -1
  22. data/lib/compo.rb +4 -21
  23. data/spec/array_branch_spec.rb +4 -2
  24. data/spec/array_composite_shared_examples.rb +2 -2
  25. data/spec/array_composite_spec.rb +1 -1
  26. data/spec/branch_shared_examples.rb +38 -5
  27. data/spec/branch_spec.rb +1 -1
  28. data/spec/composite_shared_examples.rb +1 -1
  29. data/spec/composite_spec.rb +1 -1
  30. data/spec/constant_branch_spec.rb +18 -0
  31. data/spec/hash_branch_spec.rb +4 -2
  32. data/spec/hash_composite_shared_examples.rb +3 -3
  33. data/spec/hash_composite_spec.rb +1 -1
  34. data/spec/leaf_branch_spec.rb +9 -0
  35. data/spec/{null_composite_shared_examples.rb → leaf_composite_shared_examples.rb} +1 -1
  36. data/spec/leaf_composite_spec.rb +7 -0
  37. data/spec/movable_shared_examples.rb +3 -3
  38. data/spec/movable_spec.rb +1 -1
  39. data/spec/parent_tracker_spec.rb +2 -15
  40. data/spec/parentless_spec.rb +2 -2
  41. data/spec/url_finder_shared_examples.rb +104 -0
  42. data/spec/url_finder_spec.rb +25 -114
  43. data/spec/url_referenceable_spec.rb +1 -1
  44. metadata +30 -21
  45. data/lib/compo/array_branch.rb +0 -16
  46. data/lib/compo/array_composite.rb +0 -103
  47. data/lib/compo/branch.rb +0 -18
  48. data/lib/compo/composite.rb +0 -181
  49. data/lib/compo/hash_branch.rb +0 -17
  50. data/lib/compo/hash_composite.rb +0 -70
  51. data/lib/compo/leaf.rb +0 -14
  52. data/lib/compo/movable.rb +0 -53
  53. data/lib/compo/null_composite.rb +0 -62
  54. data/lib/compo/parent_tracker.rb +0 -80
  55. data/lib/compo/parentless.rb +0 -131
  56. data/lib/compo/url_finder.rb +0 -164
  57. data/lib/compo/url_referenceable.rb +0 -68
  58. data/spec/leaf_spec.rb +0 -9
  59. data/spec/null_composite_spec.rb +0 -7
@@ -0,0 +1,133 @@
1
+ require 'compo/composites/composite'
2
+
3
+ module Compo
4
+ module Composites
5
+ # A Composite that represents the non-existent parent of an orphan
6
+ #
7
+ # Parentless is the parent assigned when an object is removed from a
8
+ # Composite, and should be the default parent of an object that can be
9
+ # added to one. It exists to make some operations easier, such as URL
10
+ # creation.
11
+ class Parentless
12
+ include Composite
13
+
14
+ # Creates a new instance of Parentless and adds an item to it
15
+ #
16
+ # This effectively removes the item's parent.
17
+ #
18
+ # If this method is passed nil, then nothing happens.
19
+ #
20
+ # @api public
21
+ # @example Makes a new Parentless for an item.
22
+ # Parentless.for(item)
23
+ # @example Does nothing.
24
+ # Parentless.for(nil)
25
+ #
26
+ # @param item [Object] The item to be reparented to a Parentless.
27
+ #
28
+ # @return [void]
29
+ def self.for(item)
30
+ new.add(nil, item) unless item.nil?
31
+ end
32
+
33
+ # 'Removes' a child from this Parentless
34
+ #
35
+ # This always succeeds, and never triggers any other action.
36
+ #
37
+ # @api public
38
+ # @example 'Removes' a child.
39
+ # parentless.remove(child)
40
+ #
41
+ # @param child [Object] The child to 'remove' from this Parentless.
42
+ #
43
+ # @return [Object] The child.
44
+ def remove(child)
45
+ child
46
+ end
47
+
48
+ # Returns the empty hash
49
+ #
50
+ # @api public
51
+ # @example Gets the children
52
+ # parentless.children
53
+ # #=> {}
54
+ #
55
+ # @return [Hash] The empty hash.
56
+ def children
57
+ {}
58
+ end
59
+
60
+ # Returns the URL of this Parentless
61
+ #
62
+ # This is always the empty string.
63
+ #
64
+ # @api public
65
+ # @example Gets the URL of a Parentless
66
+ # parentless.url
67
+ # #=> ''
68
+ #
69
+ # @return [Hash] The empty string.
70
+ def url
71
+ ''
72
+ end
73
+
74
+ # Given the ID of a child in this Parentless, returns that child's URL
75
+ #
76
+ # This is always the empty string. This is so that children of orphan
77
+ # objects have URLs starting with /their_id.
78
+ #
79
+ # @api public
80
+ # @example Gets the URL of the child of a Parentless.
81
+ # parentless.child_url(:child_id)
82
+ # #=> ''
83
+ #
84
+ # @return [Hash] The empty string.
85
+ def child_url(_)
86
+ ''
87
+ end
88
+
89
+ # Returns the parent of this Parentless
90
+ #
91
+ # This is always the same Parentless, for convenience's sake.
92
+ # Technically, as a null object, Parentless has no parent.
93
+ #
94
+ # @api public
95
+ # @example Gets the 'parent' of a Parentless.
96
+ # parentless.parent
97
+ #
98
+ # @return [self]
99
+ def parent
100
+ self
101
+ end
102
+
103
+ protected
104
+
105
+ # 'Adds' a child to this Parentless
106
+ #
107
+ # This always succeeds.
108
+ #
109
+ # @api private
110
+ #
111
+ # @param id [Object] Ignored.
112
+ # @param child [Object] The object to 'add' to this Parentless.
113
+ #
114
+ # @return [Object] The child.
115
+ def add!(_, child)
116
+ child
117
+ end
118
+
119
+ # Creates an ID function for the given child
120
+ #
121
+ # The returned proc is O(1), and always returns nil.
122
+ #
123
+ # @api private
124
+ #
125
+ # @param child [Object] The child whose ID is to be returned by the proc.
126
+ #
127
+ # @return [Proc] A proc returning nil.
128
+ def id_function(_)
129
+ -> { nil }
130
+ end
131
+ end
132
+ end
133
+ end
@@ -0,0 +1,20 @@
1
+ require 'compo/composites/composite'
2
+ require 'compo/composites/array'
3
+ require 'compo/composites/hash'
4
+ require 'compo/composites/leaf'
5
+ require 'compo/composites/parentless'
6
+
7
+ module Compo
8
+ # Submodule containing implementations of composite objects
9
+ #
10
+ # These classes only implement the concept of an object containing children,
11
+ # and those children being stored in such a way that they can be retrieved by
12
+ # ID. For the full Compo experience, including parent tracking, URL
13
+ # referencing, and moving of objects between parents, see the Branches
14
+ # submodule.
15
+ #
16
+ # The Composites are the lowest level of Compo; everything else is an
17
+ # extension to them.
18
+ module Composites
19
+ end
20
+ end
@@ -0,0 +1,166 @@
1
+ module Compo
2
+ module Finders
3
+ # A method object for finding an item in a composite tree via its URL
4
+ #
5
+ # These are *not* thread-safe.
6
+ class Url
7
+ # Initialises an URL finder
8
+ #
9
+ # @api public
10
+ # @example Initialises an UrlFinder with default missing resource
11
+ # handling.
12
+ # UrlFinder.new(composite, 'a/b/c')
13
+ # @example Initialises an UrlFinder returning a default value.
14
+ # UrlFinder.new(composite, 'a/b/c', missing_proc=->(_) { :default })
15
+ #
16
+ # @param root [Composite] A composite object serving as the root of the
17
+ # search tree, and the URL.
18
+ #
19
+ # @param url [String] A partial URL that follows this model object's URL
20
+ # to form the URL of the resource to locate. Can be nil, in which case
21
+ # this object is returned.
22
+ #
23
+ # @param missing_proc [Proc] A proc to call, with the requested URL, if
24
+ # the resource could not be found. If nil (the default), this raises a
25
+ # string exception.
26
+ def initialize(root, url, missing_proc: nil)
27
+ @root = root
28
+ @url = url
29
+ @missing_proc = missing_proc || method(:default_missing_proc)
30
+
31
+ reset
32
+ end
33
+
34
+ # Finds the model object at a URL, given a model root
35
+ #
36
+ # @api public
37
+ # @example Finds a URL with default missing resource handling.
38
+ # UrlFinder.find(composite, 'a/b/c') { |item| p item }
39
+ #
40
+ # @param (see #initialize)
41
+ #
42
+ # @yieldparam (see #run)
43
+ #
44
+ # @return [Object] The return value of the block.
45
+ def self.find(*args, &block)
46
+ new(*args).run(&block)
47
+ end
48
+
49
+ # Attempts to find a child resource with the given partial URL
50
+ #
51
+ # If the resource is found, it will be yielded to the attached block;
52
+ # otherwise, an exception will be raised.
53
+ #
54
+ # @api public
55
+ # @example Runs an UrlFinder, returning the item unchanged.
56
+ # finder.run { |item| item }
57
+ # #=> item
58
+ #
59
+ # @yieldparam resource [ModelObject] The resource found.
60
+ # @yieldparam args [Array] The splat from above.
61
+ #
62
+ # @return [Object] The return value of the block.
63
+ def run
64
+ # We're traversing down the URL by repeatedly splitting it into its
65
+ # head (part before the next /) and tail (part after). While we still
66
+ # have a tail, then the URL still needs walking down.
67
+ reset
68
+ descend until hit_end_of_url?
69
+ yield @resource
70
+ end
71
+
72
+ private
73
+
74
+ # Performs a descending step in the URL finder
75
+ #
76
+ # This tries to move down a level of the URL hierarchy, fetches the
77
+ # resource at that level, and fails according to @missing_proc if there is
78
+ # no such resource.
79
+ #
80
+ # @api private
81
+ #
82
+ # @return [Void]
83
+ def descend
84
+ descend_url
85
+ next_resource
86
+ fail_with_no_resource if @resource.nil?
87
+ end
88
+
89
+ # Seeks to the next resource pointed at by @next_id
90
+ #
91
+ # @api private
92
+ #
93
+ # @return [Void]
94
+ def next_resource
95
+ @resource = @resource.get_child_such_that { |id| id.to_s == @next_id }
96
+ end
97
+
98
+ # Fails, using @missing_proc, due to a missing resource
99
+ #
100
+ # @api private
101
+ #
102
+ # @return [Void]
103
+ def fail_with_no_resource
104
+ # If the proc returns a value instead of raising an error, then set
105
+ # things up so that value is yielded in place of the missing resource.
106
+ @tail = nil
107
+ @resource = @missing_proc.call(@url)
108
+ end
109
+
110
+ # Default value for @missing_proc
111
+ #
112
+ # @api private
113
+ #
114
+ # @param url [String] The URL whose finding failed.
115
+ #
116
+ # @return [Void]
117
+ def default_missing_proc(url)
118
+ fail("Could not find resource: #{url}")
119
+ end
120
+
121
+ # Decides whether we have reached the end of the URL
122
+ #
123
+ # @api private
124
+ #
125
+ # @return [Boolean] Whether we have hit the end of the URL.
126
+ def hit_end_of_url?
127
+ @tail.nil? || @tail.empty?
128
+ end
129
+
130
+ # Splits the tail on the next URL level
131
+ #
132
+ # @api private
133
+ #
134
+ # @return [Void]
135
+ def descend_url
136
+ @next_id, @tail = @tail.split('/', 2)
137
+ end
138
+
139
+ # Resets this UrlFinder so it can be used again
140
+ #
141
+ # @api private
142
+ #
143
+ # @return [Void]
144
+ def reset
145
+ @next_id, @tail = nil, trimmed_url
146
+ @resource = @root
147
+ end
148
+
149
+ # Removes any leading or trailing slash from the URL, returning the result
150
+ #
151
+ # This only removes one leading or trailing slash. Thus, '///' will be
152
+ # returned as '/'.
153
+ #
154
+ # @api private
155
+ #
156
+ # @return [String] The URL with no trailing or leading slash.
157
+ def trimmed_url
158
+ first, last = 0, @url.length
159
+ first += 1 if @url.start_with?('/')
160
+ last -= 1 if @url.end_with?('/')
161
+
162
+ @url[first...last]
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,10 @@
1
+ require 'compo/finders/url'
2
+
3
+ module Compo
4
+ # Module implementing the finders
5
+ #
6
+ # A finder is a method object that performs some form of traversal on a
7
+ # branch or composite.
8
+ module Finders
9
+ end
10
+ end
@@ -0,0 +1,55 @@
1
+ module Compo
2
+ module Mixins
3
+ # Helper mixin for objects that can be moved into other objects
4
+ #
5
+ # This mixin defines a method, #move_to, which handles removing a child
6
+ # from its current parent and adding it to a new one.
7
+ #
8
+ # It expects the current parent to be reachable from #parent.
9
+ module Movable
10
+ # Moves this model object to a new parent with a new ID
11
+ #
12
+ # @api public
13
+ # @example Moves the object to a new parent.
14
+ # movable.move_to(parent, :id)
15
+ # @example Moves the object out of its parent (deleting it, if there are
16
+ # no other live references).
17
+ # movable.move_to(nil, nil)
18
+ #
19
+ # @param new_parent [ModelObject] The new parent for this object (can be
20
+ # nil).
21
+ # @param new_id [Object] The new ID under which the object will exist in
22
+ # the parent.
23
+ #
24
+ # @return [self]
25
+ def move_to(new_parent, new_id)
26
+ move_from_old_parent
27
+ move_to_new_parent(new_parent, new_id)
28
+ self
29
+ end
30
+
31
+ private
32
+
33
+ # Performs the move from an old parent, if necessary
34
+ #
35
+ # @api private
36
+ #
37
+ # @return [void]
38
+ def move_from_old_parent
39
+ parent.remove(self)
40
+ end
41
+
42
+ # Performs the move to a new parent, if necessary
43
+ #
44
+ # @api private
45
+ #
46
+ # @param new_parent [Composite] The target parent of the move.
47
+ # @param new_id [Object] The intended new ID of this child.
48
+ #
49
+ # @return [void]
50
+ def move_to_new_parent(new_parent, new_id)
51
+ new_parent.add(new_id, self) unless new_parent.nil?
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,74 @@
1
+ require 'forwardable'
2
+ require 'compo/composites/parentless'
3
+
4
+ module Compo
5
+ module Mixins
6
+ # Basic implementation of parent tracking as a mixin
7
+ #
8
+ # Adding this to a Composite allows the composite to be aware of its current
9
+ # parent.
10
+ #
11
+ # This implements #parent, #update_parent and #remove_parent to track the
12
+ # current parent and ID function as instance variables. It also implements
13
+ # #parent, and #id in terms of the ID function.
14
+ module ParentTracker
15
+ extend Forwardable
16
+
17
+ # Initialises the ParentTracker
18
+ #
19
+ # This constructor sets the tracker up so it initially has an instance of
20
+ # Parentless as its 'parent'.
21
+ #
22
+ # @api semipublic
23
+ # @example Creates a new ParentTracker.
24
+ # ParentTracker.new
25
+ #
26
+ # @return [Void]
27
+ def initialize
28
+ super()
29
+ Compo::Composites::Parentless.for(self)
30
+ end
31
+
32
+ # Gets this object's current ID
33
+ #
34
+ # @api public
35
+ # @example Gets the object's parent while it has none.
36
+ # parent_tracker.parent
37
+ # #=> nil
38
+ # @example Gets the object's parent while it has one.
39
+ # parent_tracker.parent
40
+ # #=> :the_current_parent
41
+ #
42
+ # @return [Composite] The current parent.
43
+ attr_reader :parent
44
+
45
+ # Gets this object's current ID
46
+ #
47
+ # @api public
48
+ # @example Gets the object's ID while it has no parent.
49
+ # parent_tracker.id
50
+ # #=> nil
51
+ # @example Gets the object's ID while it has a parent.
52
+ # parent_tracker.id
53
+ # #=> :the_current_id
54
+ #
55
+ # @return [Object] The current ID.
56
+ def_delegator :@id_proc, :call, :id
57
+
58
+ # Updates this object's parent and ID function
59
+ #
60
+ # @api public
61
+ # @example Update this Leaf's parent and ID function.
62
+ # parent_tracker.update_parent(new_parent, new_id_function)
63
+ #
64
+ # @return [void]
65
+ def update_parent(new_parent, new_id_proc)
66
+ fail 'Parent cannot be nil: use #remove_parent.' if new_parent.nil?
67
+ fail 'ID function cannot be nil: use -> { nil }.' if new_id_proc.nil?
68
+
69
+ @parent = new_parent
70
+ @id_proc = new_id_proc
71
+ end
72
+ end
73
+ end
74
+ end
@@ -0,0 +1,70 @@
1
+ module Compo
2
+ module Mixins
3
+ # Adds ID-based 'URL's to Compo classes
4
+ #
5
+ # For the purposes of this module, a URL is a string of slash-delimited IDs
6
+ # representing the location of a Composite in the tree structure formed by
7
+ # its ancestors. Depending on the types of IDs used in the structure, the
8
+ # URLs may not actually be literal Uniform Resource Locators.
9
+ #
10
+ # This module expects its includer to define #parent and #id. These are
11
+ # defined, for example, by the Compo::ParentTracker mixin.
12
+ module UrlReferenceable
13
+ extend Forwardable
14
+
15
+ # Returns the URL of this object
16
+ #
17
+ # The #url of a Composite is defined inductively as '' for composites that
18
+ # have no parent, and the joining of the parent URL and the current ID
19
+ # otherwise.
20
+ #
21
+ # The result of #url can be used to give a URL hierarchy to Composites.
22
+ #
23
+ # @api public
24
+ # @example Gets the URL of an object with no parent.
25
+ # orphan.url
26
+ # #=> ''
27
+ # @example Gets the URL of an object with a parent.
28
+ # leaf.url
29
+ # #=> 'grandparent_id/parent_id/id'
30
+ #
31
+ # @return [String] The URL of this object.
32
+ def url
33
+ parent.child_url(id)
34
+ end
35
+
36
+ # Returns the URL of a child of this object, with the given ID
37
+ #
38
+ # This defaults to joining the ID to this object's URL with a slash.
39
+ #
40
+ # @api public
41
+ # @example Gets the URL of the child of an object without a parent.
42
+ # orphan.child_url(:id)
43
+ # #=> '/id'
44
+ # @example Gets the URL of the child of an object with a parent.
45
+ # leaf.child_url(:id)
46
+ # #=> 'grandparent_id/parent_id/id'
47
+ #
48
+ # @param child_id [Object] The ID of the child whose URL is sought.
49
+ #
50
+ # @return [String] The URL of the child with the given ID.
51
+ def child_url(child_id)
52
+ [url, child_id].join('/')
53
+ end
54
+
55
+ # Returns the URL of this object's parent
56
+ #
57
+ # @api public
58
+ # @example Gets the parent URL of an object with no parent.
59
+ # orphan.parent_url
60
+ # #=> nil
61
+ # @example Gets the URL of an object with a parent.
62
+ # leaf.parent_url
63
+ # #=> 'grandparent_id/parent_id'
64
+ #
65
+ # @return [String] The URL of this object's parent, or nil if there is no
66
+ # parent.
67
+ def_delegator :parent, :url, :parent_url
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,9 @@
1
+ require 'compo/mixins/movable'
2
+ require 'compo/mixins/parent_tracker'
3
+ require 'compo/mixins/url_referenceable'
4
+
5
+ module Compo
6
+ # The module containing the various mixins that implement composite patterns
7
+ module Mixins
8
+ end
9
+ 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.1'
3
+ VERSION = '0.4.0'
4
4
  end
data/lib/compo.rb CHANGED
@@ -1,24 +1,7 @@
1
- # Base mixins
2
- require 'compo/composite'
3
- require 'compo/movable'
4
- require 'compo/parent_tracker'
5
- require 'compo/url_referenceable'
6
-
7
- # Composite implementations
8
- require 'compo/array_composite'
9
- require 'compo/hash_composite'
10
- require 'compo/null_composite'
11
- require 'compo/parentless'
12
-
13
- # Leaf and branch classes
14
- require 'compo/array_branch'
15
- require 'compo/hash_branch'
16
- require 'compo/leaf'
17
-
18
- # Utilities
19
- require 'compo/url_finder'
20
-
21
- # Misc
1
+ require 'compo/composites'
2
+ require 'compo/mixins'
3
+ require 'compo/branches'
4
+ require 'compo/finders'
22
5
  require 'compo/version'
23
6
 
24
7
  # The main module for Compo
@@ -3,7 +3,9 @@ require 'compo'
3
3
  require 'array_composite_shared_examples'
4
4
  require 'branch_shared_examples'
5
5
 
6
- describe Compo::ArrayBranch do
7
- it_behaves_like 'a branch'
6
+ describe Compo::Branches::Array do
7
+ it_behaves_like 'a branch with children' do
8
+ let(:initial_ids) { [0, 1] }
9
+ end
8
10
  it_behaves_like 'an array composite'
9
11
  end
@@ -123,7 +123,7 @@ shared_examples 'an array composite' do
123
123
 
124
124
  it 'calls #update_parent on the child with a Parentless' do
125
125
  expect(child1).to receive(:update_parent).once do |parent, _|
126
- expect(parent).to be_a(Compo::Parentless)
126
+ expect(parent).to be_a(Compo::Composites::Parentless)
127
127
  end
128
128
  subject.remove(child1)
129
129
  end
@@ -171,7 +171,7 @@ shared_examples 'an array composite' do
171
171
 
172
172
  it 'calls #update_parent on the child with a Parentless' do
173
173
  expect(child1).to receive(:update_parent).once do |parent, _|
174
- expect(parent).to be_a(Compo::Parentless)
174
+ expect(parent).to be_a(Compo::Composites::Parentless)
175
175
  end
176
176
  subject.remove_id(0)
177
177
  end
@@ -2,6 +2,6 @@ require 'spec_helper'
2
2
  require 'compo'
3
3
  require 'array_composite_shared_examples'
4
4
 
5
- describe Compo::ArrayComposite do
5
+ describe Compo::Composites::Array do
6
6
  it_behaves_like 'an array composite'
7
7
  end
@@ -1,3 +1,4 @@
1
+ require 'url_finder_shared_examples'
1
2
  require 'url_referenceable_shared_examples'
2
3
  require 'movable_shared_examples'
3
4
 
@@ -7,7 +8,7 @@ shared_examples 'a branch' do
7
8
 
8
9
  describe '#initialize' do
9
10
  it 'initialises with a Parentless as parent' do
10
- expect(subject.parent).to be_a(Compo::Parentless)
11
+ expect(subject.parent).to be_a(Compo::Composites::Parentless)
11
12
  end
12
13
 
13
14
  it 'initialises with an ID function returning nil' do
@@ -22,10 +23,10 @@ shared_examples 'a branch' do
22
23
  end
23
24
  end
24
25
  context 'when the Branch is the child of a root' do
25
- let(:parent) { Compo::HashBranch.new }
26
+ let(:parent) { Compo::Branches::Hash.new }
26
27
  before(:each) { subject.move_to(parent, :id) }
27
28
 
28
- it 'returns /ID, where ID is the ID of the Leaf' do
29
+ it 'returns /ID, where ID is the ID of the Branch' do
29
30
  expect(subject.url).to eq('/id')
30
31
  end
31
32
  end
@@ -34,11 +35,13 @@ shared_examples 'a branch' do
34
35
  describe '#move_to' do
35
36
  context 'when the Branch has a parent' do
36
37
  context 'when the new parent is nil' do
37
- let(:parent) { Compo::HashBranch.new }
38
+ let(:parent) { Compo::Branches::Hash.new }
38
39
  before(:each) { subject.move_to(parent, :id) }
39
40
 
40
41
  it 'loses its previous parent' do
41
- expect(subject.move_to(nil, :id).parent).to be_a(Compo::Parentless)
42
+ expect(subject.move_to(nil, :id).parent).to be_a(
43
+ Compo::Composites::Parentless
44
+ )
42
45
  end
43
46
 
44
47
  it 'is no longer a child of its parent' do
@@ -49,3 +52,33 @@ shared_examples 'a branch' do
49
52
  end
50
53
  end
51
54
  end
55
+
56
+ shared_examples 'a branch with children' do
57
+ it_behaves_like 'a branch'
58
+
59
+ describe '#find_url' do
60
+ it_behaves_like 'a URL finding' do
61
+ let(:target) { Compo::Branches::Leaf.new }
62
+
63
+ before(:each) do
64
+ a = Compo::Branches::Hash.new
65
+ b = Compo::Branches::Array.new
66
+ d = Compo::Branches::Leaf.new
67
+ e = Compo::Branches::Leaf.new
68
+ zero = Compo::Branches::Leaf.new
69
+
70
+ a.move_to(subject, initial_ids[0])
71
+ b.move_to(a, 'b')
72
+ zero.move_to(b, 0)
73
+ target.move_to(b, 1)
74
+ d.move_to(subject, initial_ids[1])
75
+ e.move_to(a, 'e')
76
+ end
77
+
78
+ let(:correct_url) { "#{initial_ids[0]}/b/1" }
79
+ let(:incorrect_url) { "#{initial_ids[0]}/z/1" }
80
+
81
+ let(:proc) { ->(*args, &b) { subject.find_url(*args, &b) } }
82
+ end
83
+ end
84
+ end