storexplore 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. checksums.yaml.gz.sig +0 -0
  3. data/.gitignore +26 -0
  4. data/.rspec +1 -0
  5. data/Gemfile +4 -0
  6. data/Guardfile +9 -0
  7. data/LICENSE +165 -0
  8. data/README.md +29 -0
  9. data/Rakefile +1 -0
  10. data/lib/storexplore/api.rb +63 -0
  11. data/lib/storexplore/api_builder.rb +68 -0
  12. data/lib/storexplore/array_utils.rb +36 -0
  13. data/lib/storexplore/browsing_error.rb +26 -0
  14. data/lib/storexplore/digger.rb +35 -0
  15. data/lib/storexplore/hash_utils.rb +56 -0
  16. data/lib/storexplore/null_digger.rb +30 -0
  17. data/lib/storexplore/testing/api_shared_examples.rb +140 -0
  18. data/lib/storexplore/testing/configuration.rb +56 -0
  19. data/lib/storexplore/testing/dummy_data.rb +67 -0
  20. data/lib/storexplore/testing/dummy_store.rb +195 -0
  21. data/lib/storexplore/testing/dummy_store_api.rb +54 -0
  22. data/lib/storexplore/testing/dummy_store_constants.rb +31 -0
  23. data/lib/storexplore/testing/dummy_store_generator.rb +65 -0
  24. data/lib/storexplore/testing/matchers/have_unique_matcher.rb +74 -0
  25. data/lib/storexplore/testing/matchers/mostly_matcher.rb +45 -0
  26. data/lib/storexplore/testing.rb +30 -0
  27. data/lib/storexplore/uri_utils.rb +38 -0
  28. data/lib/storexplore/version.rb +24 -0
  29. data/lib/storexplore/walker.rb +84 -0
  30. data/lib/storexplore/walker_page.rb +142 -0
  31. data/lib/storexplore/walker_page_error.rb +25 -0
  32. data/lib/storexplore.rb +34 -0
  33. data/spec/lib/storexplore/api_builder_spec.rb +99 -0
  34. data/spec/lib/storexplore/api_spec.rb +44 -0
  35. data/spec/lib/storexplore/digger_spec.rb +53 -0
  36. data/spec/lib/storexplore/store_walker_page_spec_fixture.html +21 -0
  37. data/spec/lib/storexplore/testing/dummy_store_api_spec.rb +120 -0
  38. data/spec/lib/storexplore/uri_utils_spec.rb +51 -0
  39. data/spec/lib/storexplore/walker_page_spec.rb +120 -0
  40. data/spec/lib/storexplore/walker_spec.rb +97 -0
  41. data/spec/spec_helper.rb +28 -0
  42. data/storexplore.gemspec +27 -0
  43. data.tar.gz.sig +0 -0
  44. metadata +187 -0
  45. metadata.gz.sig +0 -0
@@ -0,0 +1,140 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # api_shared_examples.rb
4
+ #
5
+ # Copyright (c) 2010, 2011, 2012, 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+ module Storexplore
23
+ module Testing
24
+
25
+ module ApiSpecMacros
26
+
27
+ def self.included(base)
28
+ base.send :extend, ClassMethods
29
+ end
30
+
31
+ module ClassMethods
32
+
33
+ def it_should_behave_like_any_store_items_api
34
+
35
+ before :all do
36
+ @range = 0..1
37
+
38
+ generate_store
39
+ explore_store
40
+ end
41
+
42
+ it "should have many item categories" do
43
+ # TODO remove .first(3) once rspec handles lazy enums
44
+ expect(@store.categories.first(3)).to have_at_least(3).items
45
+ end
46
+
47
+ it "should have many items" do
48
+ expect(sample_items).to have_at_least(3).items
49
+ end
50
+
51
+ it "should have item categories with different names" do
52
+ categories_attributes = sample_categories.map { |cat| cat.attributes }
53
+ expect(categories_attributes).to mostly have_unique(:name).in(categories_attributes)
54
+ end
55
+
56
+ it "should have items with different names" do
57
+ expect(sample_items_attributes).to mostly have_unique(:name).in(sample_items_attributes)
58
+ end
59
+
60
+ it "should have parseable item category attributes" do
61
+ expect(parseable_categories_attributes).to mostly have_key(:name)
62
+ end
63
+
64
+ it "should have some valid item attributes" do
65
+ expect(sample_items_attributes).not_to be_empty
66
+ end
67
+
68
+ it "should have items with a price" do
69
+ expect(sample_items_attributes).to all_ { have_key(:price) }
70
+ end
71
+
72
+ it "should mostly have items with an image" do
73
+ expect(sample_items_attributes).to mostly have_key(:image)
74
+ end
75
+
76
+ it "should mostly have items with a brand" do
77
+ expect(sample_items_attributes).to mostly have_key(:brand)
78
+ end
79
+
80
+ it "should have items with unique remote id" do
81
+ expect(sample_items_attributes).to all_ { have_unique(:remote_id).in(sample_items_attributes) }
82
+ end
83
+
84
+ it "should have items with unique uris" do
85
+ expect(valid_sample_items).to mostly have_unique(:uri).in(valid_sample_items)
86
+ end
87
+ end
88
+ end
89
+
90
+ def generate_store
91
+ # by default, the store already exists
92
+ end
93
+
94
+ def explore_store
95
+ @sample_categories = dig([@store], :categories)
96
+ @all_sample_categories, @sample_items = dig_deep(@sample_categories)
97
+ @valid_sample_items = valid_items(@sample_items)
98
+ @sample_items_attributes = (valid_sample_items.map &:attributes).uniq
99
+ @parseable_categories_attributes = all_sample_categories.map do |category|
100
+ category.attributes rescue {}
101
+ end
102
+ end
103
+
104
+ attr_accessor :sample_categories, :all_sample_categories, :sample_items, :valid_sample_items, :sample_items_attributes, :parseable_categories_attributes
105
+
106
+ def dig(categories, message)
107
+ categories.map { |cat| cat.send(message).to_a[@range] }.flatten
108
+ end
109
+
110
+ def dig_deep(categories)
111
+ all_categories = [categories]
112
+
113
+ while (items = dig(categories, :items)).empty?
114
+ categories = dig(categories, :categories)
115
+ all_categories << categories
116
+ end
117
+
118
+ [all_categories.flatten, items]
119
+ end
120
+
121
+ def valid_items(items)
122
+ result = []
123
+ sample_items.each do |item|
124
+ begin
125
+ item.attributes
126
+ result.push(item)
127
+ rescue BrowsingError => e
128
+ Testing.logger.debug e.message
129
+ end
130
+ end
131
+ result
132
+ end
133
+
134
+ def logger
135
+ Testing.config.logger
136
+ end
137
+
138
+ end
139
+ end
140
+ end
@@ -0,0 +1,56 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # configuration.rb
4
+ #
5
+ # Copyright (c) 2010, 2011, 2012, 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+ require 'logger'
23
+
24
+ module Storexplore
25
+ module Testing
26
+
27
+ def self.config
28
+ @config ||= Configuration.new
29
+ yield @config if block_given?
30
+ @config
31
+ end
32
+
33
+ class Configuration
34
+
35
+ def initialize
36
+ @logger = Logger.new(STDOUT)
37
+ @logger.level = Logger::INFO
38
+ end
39
+
40
+ # Generation directory where the dummy stores will be generated.
41
+ # A sub folder with name DummyStore::NAME will be created there to hold all generated dummy stores,
42
+ # the content of this directory will be deleted when the DummyStore.wipeout method is called
43
+ def dummy_store_generation_dir
44
+ raise StandardError.new('You need to configure a dummy store generation directory with Storexplore::Testing.config.dummy_store_generation_dir=') if @generation_dir.nil?
45
+ @generation_dir
46
+ end
47
+ def dummy_store_generation_dir=(generation_dir)
48
+ @generation_dir = generation_dir
49
+ end
50
+
51
+ # Logger for custom test messages
52
+ attr_accessor :logger
53
+
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,67 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # dummy_data.rb
4
+ #
5
+ # Copyright (c) 2012, 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+ module Storexplore
23
+ module Testing
24
+
25
+ class DummyData
26
+
27
+ def self.name(kind)
28
+ "#{kind.capitalize}-#{new_int}"
29
+ end
30
+
31
+ def self.attributes(name, options)
32
+ {
33
+ name: name,
34
+ brand: brand(name),
35
+ image: image(name),
36
+ remote_id: remote_id,
37
+ price: price(name)
38
+ }.merge(options)
39
+ end
40
+
41
+ private
42
+
43
+ def self.brand(name)
44
+ "#{name} Inc."
45
+ end
46
+ def self.image(name)
47
+ "http://www.photofabric.com/#{name}"
48
+ end
49
+ def self.remote_id
50
+ new_int.to_s
51
+ end
52
+ def self.price(name)
53
+ hash = name.hash.abs
54
+ digits = Math.log(hash, 10).round
55
+ (hash/10.0**(digits-2)).round(2)
56
+ end
57
+
58
+ def self.new_int
59
+ @last_int ||= 0
60
+ result = @last_int
61
+ @last_int = @last_int + 1
62
+ result
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,195 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # dummy_store.rb
4
+ #
5
+ # Copyright (c) 2012, 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+ require "fileutils"
23
+
24
+ module Storexplore
25
+ module Testing
26
+
27
+ class DummyStore
28
+
29
+ def self.open(store_name)
30
+ new(root_path(store_name), store_name)
31
+ end
32
+
33
+ def self.uri(store_name)
34
+ "file://#{root_path(store_name)}"
35
+ end
36
+
37
+ def self.wipe_out
38
+ FileUtils.rm_rf(root_dir)
39
+ end
40
+ def self.wipe_out_store(store_name)
41
+ FileUtils.rm_rf(root_path(store_name))
42
+ end
43
+
44
+ def uri
45
+ "file://#{@path}"
46
+ end
47
+
48
+ attr_reader :name
49
+
50
+ def categories
51
+ _name, categories, _items, _attributes = read
52
+ categories.map do |category_name|
53
+ DummyStore.new("#{absolute_category_dir(category_name)}/index.html", category_name)
54
+ end
55
+ end
56
+ def category(category_name)
57
+ short_category_name = short_name(category_name)
58
+ add([short_category_name], [], {})
59
+ DummyStore.new("#{absolute_category_dir(short_category_name)}/index.html", category_name)
60
+ end
61
+ def remove_category(category_name)
62
+ short_category_name = short_name(category_name)
63
+ remove([short_category_name], [], [])
64
+ FileUtils.rm_rf(absolute_category_dir(short_category_name))
65
+ end
66
+
67
+ def items
68
+ _name, _categories, items, _attributes = read
69
+ items.map do |item_name|
70
+ DummyStore.new(absolute_item_file(item_name), item_name)
71
+ end
72
+ end
73
+ def item(item_name)
74
+ short_item_name = short_name(item_name)
75
+ add([], [short_item_name], {})
76
+ DummyStore.new(absolute_item_file(short_item_name), item_name)
77
+ end
78
+ def remove_item(item_name)
79
+ short_item_name = short_name(item_name)
80
+ remove([], [short_item_name], [])
81
+ FileUtils.rm_rf(absolute_item_file(short_item_name))
82
+ end
83
+
84
+ def attributes(*args)
85
+ return add_attributes(args[0]) if args.size == 1
86
+
87
+ _name, _categories, _items, attributes = read
88
+ HashUtils.internalize_keys(attributes)
89
+ end
90
+ def add_attributes(values)
91
+ add([], [], HashUtils.stringify_keys(values))
92
+ end
93
+ def remove_attributes(*attribute_names)
94
+ remove([], [], ArrayUtils.stringify(attribute_names))
95
+ end
96
+
97
+ def generate(count = 1)
98
+ DummyStoreGenerator.new([self], count)
99
+ end
100
+
101
+ private
102
+
103
+ def self.root_dir
104
+ File.join(Testing.config.dummy_store_generation_dir, DummyStoreConstants::NAME)
105
+ end
106
+
107
+ def self.root_path(store_name)
108
+ "#{root_dir}/#{store_name}/index.html"
109
+ end
110
+
111
+ def initialize(path, name)
112
+ @path = path
113
+ @name = name
114
+ if !File.exists?(path)
115
+ write(name, [], [], {})
116
+ end
117
+ end
118
+
119
+ def short_name(full_name)
120
+ full_name[0..20]
121
+ end
122
+
123
+ def absolute_category_dir(category_name)
124
+ "#{File.dirname(@path)}/#{relative_category_dir(category_name)}"
125
+ end
126
+ def relative_category_dir(category_name)
127
+ category_name
128
+ end
129
+
130
+ def absolute_item_file(item_name)
131
+ "#{File.dirname(@path)}/#{relative_item_file(item_name)}"
132
+ end
133
+ def relative_item_file(item_name)
134
+ "#{item_name}.html"
135
+ end
136
+
137
+ def add(extra_categories, extra_items, extra_attributes)
138
+ name, categories, items, attributes = read
139
+
140
+ if !ArrayUtils.contains?(categories, extra_categories) || !ArrayUtils.contains?(items, extra_items) || !HashUtils.contains?(attributes,extra_attributes)
141
+ write(name, categories + extra_categories, items + extra_items, attributes.merge(extra_attributes))
142
+ end
143
+ end
144
+ def remove(wrong_categories, wrong_items, wrong_attributes)
145
+ name, categories, items, attributes = read
146
+ write(name, categories - wrong_categories, items - wrong_items, HashUtils.without(attributes,wrong_attributes))
147
+ end
148
+
149
+ def write(name, categories, items, attributes)
150
+ FileUtils.mkdir_p(File.dirname(@path))
151
+ IO.write(@path, content(name, categories, items, attributes))
152
+ end
153
+
154
+ def content(name, categories, items, attributes)
155
+ (["<!DOCTYPE html>",
156
+ "<html><head><meta http-equiv=\"Content-Type\" content=\"text/html; charset=UTF-8\" /></head>",
157
+ "<body><h1>#{name}</h1><div id=\"categories\"><h2>Categories</h2><ul>"] +
158
+ categories.map {|cat| "<li><a class=\"category\" href=\"#{relative_category_dir(cat)}/index.html\">#{cat}</a></li>" } +
159
+ ['</ul></div><div id="items"><h2>Items</h2><ul>']+
160
+ items.map {|item| "<li><a class=\"item\" href=\"#{relative_item_file(item)}\">#{item}</a></li>" } +
161
+ ['</ul></div><div id="attributes"><h2>Attributes</h2><ul>']+
162
+ attributes.map {|key, value| "<li><span id=\"#{key}\">#{value}</span></li>" } +
163
+ ["</ul></div></body></html>"]).join("\n")
164
+ end
165
+
166
+ def read
167
+ parse(IO.readlines(@path))
168
+ end
169
+
170
+ def parse(lines)
171
+ name = ""
172
+ categories = []
173
+ items = []
174
+ attributes = {}
175
+ lines.each do |line|
176
+ name_match = /<h1>([^<]+)<\/h1>/.match(line)
177
+ sub_match = /<li><a class=\"([^\"]+)\" href=\"[^\"]+.html\">([^<]+)<\/a><\/li>/.match(line)
178
+ attr_match = /<li><span id=\"([^\"]+)\">([^<]+)<\/span><\/li>/.match(line)
179
+
180
+ if !!name_match
181
+ name = name_match[1]
182
+ elsif !!sub_match
183
+ case sub_match[1]
184
+ when "category" then categories << sub_match[2]
185
+ when "item" then items << sub_match[2]
186
+ end
187
+ elsif !!attr_match
188
+ attributes[attr_match[1]] = attr_match[2]
189
+ end
190
+ end
191
+ [name, categories, items, attributes]
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,54 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # dummy_store_api.rb
4
+ #
5
+ # Copyright (c) 2012, 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+ require_relative 'dummy_store_constants'
23
+
24
+ module Storexplore
25
+ module Testing
26
+
27
+ Storexplore::define_api DummyStoreConstants::NAME do
28
+
29
+ categories 'a.category' do
30
+ attributes do
31
+ { :name => page.get_one("h1").content }
32
+ end
33
+
34
+ categories 'a.category' do
35
+ attributes do
36
+ { :name => page.get_one("h1").content }
37
+ end
38
+
39
+ items 'a.item' do
40
+ attributes do
41
+ {
42
+ :name => page.get_one('h1').content,
43
+ :brand => page.get_one('#brand').content,
44
+ :price => page.get_one('#price').content.to_f,
45
+ :image => page.get_one('#image').content,
46
+ :remote_id => page.get_one('#remote_id').content
47
+ }
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,31 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # dummy_store_constants.rb
4
+ #
5
+ # Copyright (c) 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+ module Storexplore
23
+ module Testing
24
+
25
+ class DummyStoreConstants
26
+
27
+ NAME = 'dummy-store'
28
+
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,65 @@
1
+ # -*- coding: utf-8 -*-
2
+ #
3
+ # dummy_store_generator.rb
4
+ #
5
+ # Copyright (c) 2012, 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+ module Storexplore
23
+ module Testing
24
+
25
+ class DummyStoreGenerator
26
+ def initialize(pages, count = 1)
27
+ @pages = pages
28
+ @count = count
29
+ end
30
+
31
+ def and(count)
32
+ @count = count
33
+ self
34
+ end
35
+
36
+ def categories
37
+ dispatch(:category)
38
+ end
39
+ alias_method :category, :categories
40
+
41
+ def items
42
+ dispatch(:item).attributes
43
+ end
44
+ alias_method :item, :items
45
+
46
+ def attributes(options = {})
47
+ @pages.map do |page|
48
+ attributes = DummyData.attributes(page.name, options)
49
+ page.attributes(HashUtils.without(attributes, [:name]))
50
+ end
51
+ end
52
+
53
+ private
54
+
55
+ def dispatch(message)
56
+ sub_pages = @pages.map do |page|
57
+ @count.times.map do
58
+ page.send(message, DummyData.name(message))
59
+ end
60
+ end
61
+ DummyStoreGenerator.new(sub_pages.flatten)
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,74 @@
1
+ # -*- encoding: utf-8 -*-
2
+ #
3
+ # have_unique_matcher.rb
4
+ #
5
+ # Copyright (c) 2010, 2011, 2013 by Philippe Bourgau. All rights reserved.
6
+ #
7
+ # This library is free software; you can redistribute it and/or
8
+ # modify it under the terms of the GNU Lesser General Public
9
+ # License as published by the Free Software Foundation; either
10
+ # version 3.0 of the License, or (at your option) any later version.
11
+ #
12
+ # This library is distributed in the hope that it will be useful,
13
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
15
+ # Lesser General Public License for more details.
16
+ #
17
+ # You should have received a copy of the GNU Lesser General Public
18
+ # License along with this library; if not, write to the Free Software
19
+ # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
20
+ # MA 02110-1301 USA
21
+
22
+
23
+ # Matcher to verify that a hash's key is unique in a collection of other hashes
24
+ # a full class is required to implement the in method
25
+ # Use like: expect(hash).to have_unique(:id).in(hashes)
26
+ class HaveUnique
27
+
28
+ def initialize(key)
29
+ @key = key
30
+ end
31
+
32
+ def in(collection)
33
+ @collection = collection
34
+ @index = Hash.new(0)
35
+ collection.each do |item|
36
+ @index[value(item)] += 1
37
+ end
38
+ self
39
+ end
40
+
41
+ def matches?(actual)
42
+ @actual = actual
43
+ @index[value(actual)] == 1
44
+ end
45
+
46
+ def failure_message_for_should
47
+ "expected #{value_expression(actual)} (=#{value(actual)}) to be unique in #{@collection}"
48
+ end
49
+
50
+ def description
51
+ "expected an hash or object with a unique #{@key} in #{@collection}"
52
+ end
53
+
54
+ private
55
+ def value(actual)
56
+ if actual.instance_of?(Hash)
57
+ actual[@key]
58
+ else
59
+ actual.send(@key)
60
+ end
61
+ end
62
+ def value_expression(actual)
63
+ if actual.instance_of?(Hash)
64
+ "#{actual}[#{@key}]"
65
+ else
66
+ "#{actual}.#{@key}"
67
+ end
68
+ end
69
+ end
70
+
71
+ def have_unique(key)
72
+ HaveUnique.new(key)
73
+ end
74
+