compo 0.3.1 → 0.4.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.
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