load_balanced_tire 0.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.
Files changed (121) hide show
  1. data/.gitignore +14 -0
  2. data/.travis.yml +29 -0
  3. data/Gemfile +4 -0
  4. data/MIT-LICENSE +20 -0
  5. data/README.markdown +760 -0
  6. data/Rakefile +78 -0
  7. data/examples/rails-application-template.rb +249 -0
  8. data/examples/tire-dsl.rb +876 -0
  9. data/lib/tire.rb +55 -0
  10. data/lib/tire/alias.rb +296 -0
  11. data/lib/tire/configuration.rb +30 -0
  12. data/lib/tire/dsl.rb +43 -0
  13. data/lib/tire/http/client.rb +62 -0
  14. data/lib/tire/http/clients/curb.rb +61 -0
  15. data/lib/tire/http/clients/faraday.rb +71 -0
  16. data/lib/tire/http/response.rb +27 -0
  17. data/lib/tire/index.rb +361 -0
  18. data/lib/tire/logger.rb +60 -0
  19. data/lib/tire/model/callbacks.rb +40 -0
  20. data/lib/tire/model/import.rb +26 -0
  21. data/lib/tire/model/indexing.rb +128 -0
  22. data/lib/tire/model/naming.rb +100 -0
  23. data/lib/tire/model/percolate.rb +99 -0
  24. data/lib/tire/model/persistence.rb +71 -0
  25. data/lib/tire/model/persistence/attributes.rb +143 -0
  26. data/lib/tire/model/persistence/finders.rb +66 -0
  27. data/lib/tire/model/persistence/storage.rb +69 -0
  28. data/lib/tire/model/search.rb +307 -0
  29. data/lib/tire/results/collection.rb +114 -0
  30. data/lib/tire/results/item.rb +86 -0
  31. data/lib/tire/results/pagination.rb +54 -0
  32. data/lib/tire/rubyext/hash.rb +8 -0
  33. data/lib/tire/rubyext/ruby_1_8.rb +7 -0
  34. data/lib/tire/rubyext/symbol.rb +11 -0
  35. data/lib/tire/search.rb +188 -0
  36. data/lib/tire/search/facet.rb +74 -0
  37. data/lib/tire/search/filter.rb +28 -0
  38. data/lib/tire/search/highlight.rb +37 -0
  39. data/lib/tire/search/query.rb +186 -0
  40. data/lib/tire/search/scan.rb +114 -0
  41. data/lib/tire/search/script_field.rb +23 -0
  42. data/lib/tire/search/sort.rb +25 -0
  43. data/lib/tire/tasks.rb +135 -0
  44. data/lib/tire/utils.rb +17 -0
  45. data/lib/tire/version.rb +22 -0
  46. data/test/fixtures/articles/1.json +1 -0
  47. data/test/fixtures/articles/2.json +1 -0
  48. data/test/fixtures/articles/3.json +1 -0
  49. data/test/fixtures/articles/4.json +1 -0
  50. data/test/fixtures/articles/5.json +1 -0
  51. data/test/integration/active_model_indexing_test.rb +51 -0
  52. data/test/integration/active_model_searchable_test.rb +114 -0
  53. data/test/integration/active_record_searchable_test.rb +446 -0
  54. data/test/integration/boolean_queries_test.rb +43 -0
  55. data/test/integration/count_test.rb +34 -0
  56. data/test/integration/custom_score_queries_test.rb +88 -0
  57. data/test/integration/dis_max_queries_test.rb +68 -0
  58. data/test/integration/dsl_search_test.rb +22 -0
  59. data/test/integration/explanation_test.rb +44 -0
  60. data/test/integration/facets_test.rb +259 -0
  61. data/test/integration/filtered_queries_test.rb +66 -0
  62. data/test/integration/filters_test.rb +63 -0
  63. data/test/integration/fuzzy_queries_test.rb +20 -0
  64. data/test/integration/highlight_test.rb +64 -0
  65. data/test/integration/index_aliases_test.rb +122 -0
  66. data/test/integration/index_mapping_test.rb +43 -0
  67. data/test/integration/index_store_test.rb +96 -0
  68. data/test/integration/index_update_document_test.rb +111 -0
  69. data/test/integration/mongoid_searchable_test.rb +309 -0
  70. data/test/integration/percolator_test.rb +111 -0
  71. data/test/integration/persistent_model_test.rb +130 -0
  72. data/test/integration/prefix_query_test.rb +43 -0
  73. data/test/integration/query_return_version_test.rb +70 -0
  74. data/test/integration/query_string_test.rb +52 -0
  75. data/test/integration/range_queries_test.rb +36 -0
  76. data/test/integration/reindex_test.rb +46 -0
  77. data/test/integration/results_test.rb +39 -0
  78. data/test/integration/scan_test.rb +56 -0
  79. data/test/integration/script_fields_test.rb +38 -0
  80. data/test/integration/sort_test.rb +36 -0
  81. data/test/integration/text_query_test.rb +39 -0
  82. data/test/models/active_model_article.rb +31 -0
  83. data/test/models/active_model_article_with_callbacks.rb +49 -0
  84. data/test/models/active_model_article_with_custom_document_type.rb +7 -0
  85. data/test/models/active_model_article_with_custom_index_name.rb +7 -0
  86. data/test/models/active_record_models.rb +122 -0
  87. data/test/models/article.rb +15 -0
  88. data/test/models/mongoid_models.rb +97 -0
  89. data/test/models/persistent_article.rb +11 -0
  90. data/test/models/persistent_article_in_namespace.rb +12 -0
  91. data/test/models/persistent_article_with_casting.rb +28 -0
  92. data/test/models/persistent_article_with_defaults.rb +11 -0
  93. data/test/models/persistent_articles_with_custom_index_name.rb +10 -0
  94. data/test/models/supermodel_article.rb +17 -0
  95. data/test/models/validated_model.rb +11 -0
  96. data/test/test_helper.rb +93 -0
  97. data/test/unit/active_model_lint_test.rb +17 -0
  98. data/test/unit/configuration_test.rb +74 -0
  99. data/test/unit/http_client_test.rb +76 -0
  100. data/test/unit/http_response_test.rb +49 -0
  101. data/test/unit/index_alias_test.rb +275 -0
  102. data/test/unit/index_test.rb +894 -0
  103. data/test/unit/logger_test.rb +125 -0
  104. data/test/unit/model_callbacks_test.rb +116 -0
  105. data/test/unit/model_import_test.rb +71 -0
  106. data/test/unit/model_persistence_test.rb +528 -0
  107. data/test/unit/model_search_test.rb +913 -0
  108. data/test/unit/results_collection_test.rb +281 -0
  109. data/test/unit/results_item_test.rb +162 -0
  110. data/test/unit/rubyext_test.rb +66 -0
  111. data/test/unit/search_facet_test.rb +153 -0
  112. data/test/unit/search_filter_test.rb +42 -0
  113. data/test/unit/search_highlight_test.rb +46 -0
  114. data/test/unit/search_query_test.rb +301 -0
  115. data/test/unit/search_scan_test.rb +113 -0
  116. data/test/unit/search_script_field_test.rb +26 -0
  117. data/test/unit/search_sort_test.rb +50 -0
  118. data/test/unit/search_test.rb +499 -0
  119. data/test/unit/tire_test.rb +126 -0
  120. data/tire.gemspec +90 -0
  121. metadata +549 -0
@@ -0,0 +1,125 @@
1
+ require 'test_helper'
2
+ require 'time'
3
+
4
+ module Tire
5
+
6
+ class LoggerTest < Test::Unit::TestCase
7
+ include Tire
8
+
9
+ context "Logger" do
10
+
11
+ context "initialized with an IO object" do
12
+
13
+ should "take STDOUT" do
14
+ assert_nothing_raised do
15
+ logger = Logger.new STDOUT
16
+ end
17
+ end
18
+
19
+ should "write to STDERR" do
20
+ STDERR.expects(:write).with('BOOM!')
21
+ logger = Logger.new STDERR
22
+ logger.write('BOOM!')
23
+ end
24
+
25
+ end
26
+
27
+ context "initialized with file" do
28
+ teardown { File.delete('myfile.log') }
29
+
30
+ should "create the file" do
31
+ assert_nothing_raised do
32
+ logger = Logger.new 'myfile.log'
33
+ assert File.exists?('myfile.log')
34
+ end
35
+ end
36
+
37
+ should "write to file" do
38
+ File.any_instance.expects(:write).with('BOOM!')
39
+ logger = Logger.new 'myfile.log'
40
+ logger.write('BOOM!')
41
+ end
42
+
43
+ end
44
+
45
+ end
46
+
47
+ context "levels" do
48
+
49
+ should "have the default level" do
50
+ logger = Logger.new STDERR
51
+ assert_equal 'info', logger.level
52
+ end
53
+
54
+ should "set the level" do
55
+ logger = Logger.new STDERR, :level => 'debug'
56
+ assert_equal 'debug', logger.level
57
+ end
58
+
59
+ end
60
+
61
+ context "tracing requests" do
62
+ setup do
63
+ Time.stubs(:now).returns(Time.parse('2011-03-19 11:00'))
64
+ @logger = Logger.new STDERR
65
+ end
66
+
67
+ should "log request in correct format" do
68
+ log = (<<-"log;").gsub(/^ +/, '')
69
+ # 2011-03-19 11:00:00:000 [_search] (["articles", "users"])
70
+ #
71
+ curl -X GET http://...
72
+
73
+ log;
74
+ @logger.expects(:write).with do |payload|
75
+ payload =~ Regexp.new( Regexp.escape('2011-03-19 11:00:00') )
76
+ payload =~ Regexp.new( Regexp.escape('_search') )
77
+ payload =~ Regexp.new( Regexp.escape('(["articles", "users"])') )
78
+ end
79
+ @logger.log_request('_search', ["articles", "users"], 'curl -X GET http://...')
80
+ end
81
+
82
+ should "log response in correct format" do
83
+ json = (<<-"json;").gsub(/^\s*/, '')
84
+ {
85
+ "took" : 4,
86
+ "hits" : {
87
+ "total" : 20,
88
+ "max_score" : 1.0,
89
+ "hits" : [ {
90
+ "_index" : "articles",
91
+ "_type" : "article",
92
+ "_id" : "Hmg0B0VSRKm2VAlsasdnqg",
93
+ "_score" : 1.0, "_source" : { "title" : "Article 1", "published_on" : "2011-01-01" }
94
+ }, {
95
+ "_index" : "articles",
96
+ "_type" : "article",
97
+ "_id" : "booSWC8eRly2I06GTUilNA",
98
+ "_score" : 1.0, "_source" : { "title" : "Article 2", "published_on" : "2011-01-12" }
99
+ }
100
+ ]
101
+ }
102
+ }
103
+ json;
104
+ log = (<<-"log;").gsub(/^\s*/, '')
105
+ # 2011-03-19 11:00:00:000 [200 OK] (4 msec)
106
+ #
107
+ log;
108
+ # log += json.split.map { |line| "# #{line}" }.join("\n")
109
+ json.each_line { |line| log += "# #{line}" }
110
+ log += "\n\n"
111
+ @logger.expects(:write).with do |payload|
112
+ payload =~ Regexp.new( Regexp.escape('2011-03-19 11:00:00') )
113
+ payload =~ Regexp.new( Regexp.escape('[200 OK]') )
114
+ payload =~ Regexp.new( Regexp.escape('(4 msec)') )
115
+ payload =~ Regexp.new( Regexp.escape('took') )
116
+ payload =~ Regexp.new( Regexp.escape('hits') )
117
+ payload =~ Regexp.new( Regexp.escape('_score') )
118
+ end
119
+ @logger.log_response('200 OK', 4, json)
120
+ end
121
+
122
+ end
123
+
124
+ end
125
+ end
@@ -0,0 +1,116 @@
1
+ require 'test_helper'
2
+
3
+ class ModelOne
4
+ extend ActiveModel::Naming
5
+ include Tire::Model::Search
6
+ include Tire::Model::Callbacks
7
+
8
+ def save; false; end
9
+ def destroy; false; end
10
+ end
11
+
12
+ class ModelTwo
13
+ extend ActiveModel::Naming
14
+ extend ActiveModel::Callbacks
15
+ define_model_callbacks :save, :destroy
16
+
17
+ include Tire::Model::Search
18
+ include Tire::Model::Callbacks
19
+
20
+ def save; _run_save_callbacks {}; end
21
+ def destroy; _run_destroy_callbacks { @destroyed = true }; end
22
+
23
+ def destroyed?; !!@destroyed; end
24
+ end
25
+
26
+ class ModelThree
27
+ extend ActiveModel::Naming
28
+ extend ActiveModel::Callbacks
29
+ define_model_callbacks :save, :destroy
30
+
31
+ include Tire::Model::Search
32
+ include Tire::Model::Callbacks
33
+
34
+ def save; _run_save_callbacks {}; end
35
+ def destroy; _run_destroy_callbacks {}; end
36
+ end
37
+
38
+ class ModelWithoutTireAutoCallbacks
39
+ extend ActiveModel::Naming
40
+ extend ActiveModel::Callbacks
41
+ define_model_callbacks :save, :destroy
42
+
43
+ include Tire::Model::Search
44
+ # DO NOT include Callbacks
45
+
46
+ def save; _run_save_callbacks {}; end
47
+ def destroy; _run_destroy_callbacks {}; end
48
+ end
49
+
50
+ module Tire
51
+ module Model
52
+
53
+ class ModelCallbacksTest < Test::Unit::TestCase
54
+
55
+ context "Model without ActiveModel callbacks" do
56
+
57
+ should "not execute any callbacks" do
58
+ m = ModelOne.new
59
+ m.tire.expects(:update_index).never
60
+
61
+ m.save
62
+ m.destroy
63
+ end
64
+
65
+ end
66
+
67
+ context "Model with ActiveModel callbacks and implemented destroyed? method" do
68
+
69
+ should "execute the callbacks" do
70
+ m = ModelTwo.new
71
+ m.tire.expects(:update_index).twice
72
+
73
+ m.save
74
+ m.destroy
75
+ end
76
+
77
+ end
78
+
79
+ context "Model with ActiveModel callbacks without destroyed? method implemented" do
80
+
81
+ should "have the destroyed? method added" do
82
+ assert_respond_to ModelThree.new, :destroyed?
83
+ end
84
+
85
+ should "execute the callbacks" do
86
+ m = ModelThree.new
87
+ m.tire.expects(:update_index).twice
88
+
89
+ m.save
90
+ m.destroy
91
+ end
92
+
93
+ end
94
+
95
+ context "Model without Tire::Callbacks included" do
96
+
97
+ should "respond to Tire update_index callbacks" do
98
+ assert_respond_to ModelWithoutTireAutoCallbacks, :after_update_elasticsearch_index
99
+ assert_respond_to ModelWithoutTireAutoCallbacks, :before_update_elasticsearch_index
100
+ end
101
+
102
+ should "not execute the update_index hooks" do
103
+ m = ModelWithoutTireAutoCallbacks.new
104
+ m.tire.expects(:update_index).never
105
+
106
+ m.save
107
+ m.destroy
108
+ end
109
+ end
110
+
111
+ # ---------------------------------------------------------------------------
112
+
113
+ end
114
+
115
+ end
116
+ end
@@ -0,0 +1,71 @@
1
+ require 'test_helper'
2
+
3
+ class ImportModel
4
+ extend ActiveModel::Naming
5
+ include Tire::Model::Search
6
+ include Tire::Model::Callbacks
7
+
8
+ DATA = (1..4).to_a
9
+
10
+ def self.paginate(options={})
11
+ options = {:page => 1, :per_page => 1000}.update options
12
+ DATA.slice( (options[:page]-1)*options[:per_page]...options[:page]*options[:per_page] )
13
+ end
14
+
15
+ def self.all(options={})
16
+ DATA
17
+ end
18
+
19
+ def self.count
20
+ DATA.size
21
+ end
22
+ end
23
+
24
+ module Tire
25
+ module Model
26
+
27
+ class ImportTest < Test::Unit::TestCase
28
+
29
+ context "Model::Import" do
30
+
31
+ should "have the import method" do
32
+ assert_respond_to ImportModel, :import
33
+ end
34
+
35
+ should "paginate the results by default when importing" do
36
+ Tire::Index.any_instance.expects(:bulk_store).returns(true).times(2)
37
+
38
+ ImportModel.import :per_page => 2
39
+ end
40
+
41
+ should "call the passed block on every batch, and NOT manipulate the documents array" do
42
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [1, 2] }
43
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [3, 4] }
44
+
45
+ runs = 0
46
+ ImportModel.import :per_page => 2 do |documents|
47
+ runs += 1
48
+ # Don't forget to return the documents at the end of the block
49
+ documents
50
+ end
51
+
52
+ assert_equal 2, runs
53
+ end
54
+
55
+ should "manipulate the documents in passed block" do
56
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [2, 3] }
57
+ Tire::Index.any_instance.expects(:bulk_store).with { |c,o| c == [4, 5] }
58
+
59
+ ImportModel.import :per_page => 2 do |documents|
60
+ # Add 1 to every "document" and return them
61
+ documents.map { |d| d + 1 }
62
+ end
63
+
64
+ end
65
+
66
+ end
67
+
68
+ end
69
+
70
+ end
71
+ end
@@ -0,0 +1,528 @@
1
+ require 'test_helper'
2
+
3
+ module Tire
4
+ module Model
5
+
6
+ class PersistenceTest < Test::Unit::TestCase
7
+
8
+ context "Model" do
9
+
10
+ should "have default index name" do
11
+ assert_equal 'persistent_articles', PersistentArticle.index_name
12
+ assert_equal 'persistent_articles', PersistentArticle.new(:title => 'Test').index_name
13
+ end
14
+
15
+ should "allow to set custom index name" do
16
+ assert_equal 'custom-index-name', PersistentArticleWithCustomIndexName.index_name
17
+
18
+ PersistentArticleWithCustomIndexName.index_name "another-index-name"
19
+ assert_equal 'another-index-name', PersistentArticleWithCustomIndexName.index_name
20
+ assert_equal 'another-index-name', PersistentArticleWithCustomIndexName.index.name
21
+ end
22
+
23
+ context "with index prefix" do
24
+ setup do
25
+ Model::Search.index_prefix 'prefix'
26
+ end
27
+
28
+ teardown do
29
+ Model::Search.index_prefix nil
30
+ end
31
+
32
+ should "have configured prefix in index_name" do
33
+ assert_equal 'prefix_persistent_articles', PersistentArticle.index_name
34
+ assert_equal 'prefix_persistent_articles', PersistentArticle.new(:title => 'Test').index_name
35
+ end
36
+
37
+ end
38
+
39
+ should "have document_type" do
40
+ assert_equal 'persistent_article', PersistentArticle.document_type
41
+ assert_equal 'persistent_article', PersistentArticle.new(:title => 'Test').document_type
42
+ end
43
+
44
+ should "allow to define property" do
45
+ assert_nothing_raised do
46
+ a = PersistentArticle.new
47
+ class << a
48
+ property :status
49
+ end
50
+ end
51
+ end
52
+
53
+ end
54
+
55
+ context "Finders" do
56
+
57
+ setup do
58
+ @first = { '_id' => 1, '_version' => 1, '_index' => 'persistent_articles', '_type' => 'persistent_article', '_source' => { :title => 'First' } }
59
+ @second = { '_id' => 2, '_index' => 'persistent_articles', '_type' => 'persistent_article', '_source' => { :title => 'Second' } }
60
+ @third = { '_id' => 3, '_index' => 'persistent_articles', '_type' => 'persistent_article', '_source' => { :title => 'Third' } }
61
+ @find_all = { 'hits' => { 'hits' => [
62
+ @first,
63
+ @second,
64
+ @third
65
+ ] } }
66
+ @find_first = { 'hits' => { 'hits' => [ @first ] } }
67
+ @find_last_two = { 'hits' => { 'hits' => [ @second, @third ] } }
68
+ @find_twenty_ids = { 'hits' => { 'hits' => 20.times.map { @first } } }
69
+ end
70
+
71
+ should "find document by numeric ID" do
72
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
73
+ document = PersistentArticle.find 1
74
+
75
+ assert_instance_of PersistentArticle, document
76
+ assert_equal 1, document.id
77
+ assert_equal 1, document.attributes['id']
78
+ assert_equal 'First', document.attributes['title']
79
+ assert_equal 'First', document.title
80
+ end
81
+
82
+ should "have _type, _index, _id, _version attributes" do
83
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
84
+ document = PersistentArticle.find 1
85
+
86
+ assert_instance_of PersistentArticle, document
87
+ assert_equal 1, document.id
88
+ assert_equal 1, document.attributes['id']
89
+ assert_equal 'persistent_articles', document._index
90
+ assert_equal 'persistent_article', document._type
91
+ assert_equal 1, document._version
92
+ end
93
+
94
+ should "find document by string ID" do
95
+ Configuration.client.expects(:get).returns(mock_response(@first.to_json))
96
+ document = PersistentArticle.find '1'
97
+
98
+ assert_instance_of PersistentArticle, document
99
+ assert_equal 1, document.id
100
+ assert_equal 1, document.attributes['id']
101
+ assert_equal 'First', document.attributes['title']
102
+ assert_equal 'First', document.title
103
+ end
104
+
105
+ should "find document by list of IDs" do
106
+ Configuration.client.expects(:get).returns(mock_response(@find_last_two.to_json))
107
+ documents = PersistentArticle.find 2, 3
108
+
109
+ assert_equal 2, documents.count
110
+ end
111
+
112
+ should "find document by array of IDs" do
113
+ Configuration.client.expects(:get).returns(mock_response(@find_last_two.to_json))
114
+ documents = PersistentArticle.find [2, 3]
115
+
116
+ assert_equal 2, documents.count
117
+ end
118
+
119
+ should "find all documents listed in IDs array" do
120
+ ids = (1..20).to_a
121
+ Configuration.client.expects(:get).returns(mock_response(@find_twenty_ids.to_json))
122
+ Tire::Search::Search.any_instance.expects(:size).with(ids.size)
123
+
124
+ documents = PersistentArticle.find ids
125
+ assert_equal ids.size, documents.count
126
+ end
127
+
128
+ should "find all documents" do
129
+ Configuration.client.stubs(:get).returns(mock_response(@find_all.to_json))
130
+ documents = PersistentArticle.all
131
+
132
+ assert_equal 3, documents.count
133
+ assert_equal 'First', documents.first.attributes['title']
134
+ assert_equal PersistentArticle.find(:all).map { |e| e.id }, PersistentArticle.all.map { |e| e.id }
135
+ end
136
+
137
+ should "find first document" do
138
+ Configuration.client.expects(:get).returns(mock_response(@find_first.to_json))
139
+ document = PersistentArticle.first
140
+
141
+ assert_equal 'First', document.attributes['title']
142
+ end
143
+
144
+ should "raise error when passing incorrect argument" do
145
+ assert_raise(ArgumentError) do
146
+ PersistentArticle.find :name => 'Test'
147
+ end
148
+ end
149
+
150
+ should_eventually "raise error when document is not found" do
151
+ assert_raise(DocumentNotFound) do
152
+ PersistentArticle.find 'xyz001'
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ context "Persistent model" do
159
+
160
+ setup { @article = PersistentArticle.new :title => 'Test', :tags => [:one, :two] }
161
+
162
+ context "attribute methods" do
163
+
164
+ should "allow to set attributes on initialization" do
165
+ assert_not_nil @article.attributes
166
+ assert_equal 'Test', @article.attributes['title']
167
+ end
168
+
169
+ should "allow to leave attributes blank on initialization" do
170
+ assert_nothing_raised { PersistentArticle.new }
171
+ end
172
+
173
+ should "have getter methods for attributes" do
174
+ assert_not_nil @article.title
175
+ assert_equal 'Test', @article.title
176
+ assert_equal [:one, :two], @article.tags
177
+ end
178
+
179
+ should "have getter methods for attribute passed as a String" do
180
+ article = PersistentArticle.new 'title' => 'Tony Montana'
181
+ assert_not_nil article.title
182
+ assert_equal 'Tony Montana', article.title
183
+ end
184
+
185
+ should "raise error when getting unknown attribute" do
186
+ assert_raise(NoMethodError) do
187
+ @article.krapulitz
188
+ end
189
+ end
190
+
191
+ should "not raise error when getting unset attribute" do
192
+ article = PersistentArticle.new :title => 'Test'
193
+
194
+ assert_nothing_raised { article.published_on }
195
+ assert_nil article.published_on
196
+ end
197
+
198
+ should "return default value for attribute" do
199
+ article = PersistentArticleWithDefaults.new :title => 'Test'
200
+ assert_equal [], article.tags
201
+ assert_equal false, article.hidden
202
+ end
203
+
204
+ should "not affect default value" do
205
+ article = PersistentArticleWithDefaults.new :title => 'Test'
206
+ article.tags << "ruby"
207
+
208
+ article.options[:switches] << "switch_1"
209
+
210
+ assert_equal [], PersistentArticleWithDefaults.new.tags
211
+ assert_equal [], PersistentArticleWithDefaults.new.options[:switches]
212
+ end
213
+
214
+ should "have query method for attribute" do
215
+ assert_equal true, @article.title?
216
+ end
217
+
218
+ should "raise error when querying for unknown attribute" do
219
+ assert_raise(NoMethodError) do
220
+ @article.krapulitz?
221
+ end
222
+ end
223
+
224
+ should "not raise error when querying for unset attribute" do
225
+ article = PersistentArticle.new :title => 'Test'
226
+
227
+ assert_nothing_raised { article.published_on? }
228
+ assert ! article.published_on?
229
+ end
230
+
231
+ should "return true for respond_to? calls for set attributes" do
232
+ article = PersistentArticle.new :title => 'Test'
233
+ assert article.respond_to?(:title)
234
+ end
235
+
236
+ should "return false for respond_to? calls for unknown attributes" do
237
+ article = PersistentArticle.new :title => 'Test'
238
+ assert ! article.respond_to?(:krapulitz)
239
+ end
240
+
241
+ should "return true for respond_to? calls for defined but unset attributes" do
242
+ article = PersistentArticle.new :title => 'Test'
243
+
244
+ assert article.respond_to?(:published_on)
245
+ end
246
+
247
+ should "have attribute names" do
248
+ article = PersistentArticle.new :title => 'Test', :tags => ['one', 'two']
249
+ assert_equal ['published_on', 'tags', 'title'], article.attribute_names
250
+ end
251
+
252
+ should "have setter method for attribute" do
253
+ @article.title = 'Updated'
254
+ assert_equal 'Updated', @article.title
255
+ assert_equal 'Updated', @article.attributes['title']
256
+ end
257
+
258
+ should_eventually "allow to set deeply nested attributes on initialization" do
259
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
260
+
261
+ assert_equal 'John', article.author.first_name
262
+ assert_equal 'Smith', article.author.last_name
263
+ assert_equal({ :first_name => 'John', :last_name => 'Smith' }, article.attributes['author'])
264
+ end
265
+
266
+ should_eventually "allow to set deeply nested attributes on update" do
267
+ article = PersistentArticle.new :title => 'Test', :author => { :first_name => 'John', :last_name => 'Smith' }
268
+
269
+ article.author.first_name = 'Robert'
270
+ article.author.last_name = 'Carpenter'
271
+
272
+ assert_equal 'Robert', article.author.first_name
273
+ assert_equal 'Carpenter', article.author.last_name
274
+ assert_equal({ :first_name => 'Robert', :last_name => 'Carpenter' }, article.attributes['author'])
275
+ end
276
+
277
+ end
278
+
279
+ context "with casting" do
280
+
281
+ should "cast the value as custom class" do
282
+ article = PersistentArticleWithCastedItem.new :title => 'Test',
283
+ :author => { :first_name => 'John', :last_name => 'Smith' }
284
+ assert_instance_of Author, article.author
285
+ assert_equal 'John', article.author.first_name
286
+ end
287
+
288
+ should "cast the value as collection of custom classes" do
289
+ article = PersistentArticleWithCastedCollection.new :title => 'Test',
290
+ :comments => [{:nick => '4chan', :body => 'WHY U NO?'}]
291
+ assert_instance_of Array, article.comments
292
+ assert_instance_of Comment, article.comments.first
293
+ assert_equal '4chan', article.comments.first.nick
294
+ end
295
+
296
+ should "automatically format strings in UTC format as Time" do
297
+ article = PersistentArticle.new :published_on => '2011-11-01T23:00:00Z'
298
+ assert_instance_of Time, article.published_on
299
+ assert_equal 2011, article.published_on.year
300
+ end
301
+
302
+ should "cast anonymous Hashes as Hashr instances" do
303
+ article = PersistentArticleWithCastedItem.new :stats => { :views => 100, :meta => { :tags => 'A' } }
304
+ assert_equal 100, article.stats.views
305
+ assert_equal 'A', article.stats.meta.tags
306
+ end
307
+
308
+ end
309
+
310
+ context "when initializing" do
311
+
312
+ should "be a new record" do
313
+ article = PersistentArticle.new :title => 'Test'
314
+
315
+ assert article.new_record?, "#{article.inspect} should be `new_record?`"
316
+ assert ! article.persisted?, "#{article.inspect} should NOT be `persisted?`"
317
+ end
318
+
319
+ end
320
+
321
+ context "when creating" do
322
+
323
+ should "save the document with generated ID in the database" do
324
+ Configuration.client.expects(:post).
325
+ with do |url, payload|
326
+ doc = MultiJson.decode(payload)
327
+ url == "#{Configuration.url}/persistent_articles/persistent_article/" &&
328
+ doc['title'] == 'Test' &&
329
+ doc['tags'] == ['one', 'two']
330
+ doc['published_on'] == nil
331
+ end.
332
+ returns(mock_response('{"ok":true,"_id":"abc123","_version":1}'))
333
+ article = PersistentArticle.create :title => 'Test', :tags => [:one, :two]
334
+
335
+ assert article.persisted?, "#{article.inspect} should be `persisted?`"
336
+ assert ! article.new_record?, "#{article.inspect} should NOT be `new_record?`"
337
+ assert_equal 'abc123', article.id
338
+ end
339
+
340
+ should "save the document with custom ID in the database" do
341
+ Configuration.client.expects(:post).
342
+ with do |url, payload|
343
+ doc = MultiJson.decode(payload)
344
+ url == "#{Configuration.url}/persistent_articles/persistent_article/r2d2" &&
345
+ doc['title'] == 'Test' &&
346
+ doc['published_on'] == nil
347
+ end.
348
+ returns(mock_response('{"ok":true,"_id":"r2d2"}'))
349
+ article = PersistentArticle.create :id => 'r2d2', :title => 'Test'
350
+
351
+ assert_equal 'r2d2', article.id
352
+ end
353
+
354
+ should "perform model validations" do
355
+ Configuration.client.expects(:post).never
356
+
357
+ assert ! ValidatedModel.create(:name => nil)
358
+ end
359
+
360
+ end
361
+
362
+ context "when creating" do
363
+
364
+ should "set the id property" do
365
+ Configuration.client.expects(:post).
366
+ with do |url, payload|
367
+ doc = MultiJson.decode(payload)
368
+ url == "#{Configuration.url}/persistent_articles/persistent_article/" &&
369
+ doc['title'] == 'Test'
370
+ end.
371
+ returns(mock_response('{"ok":true,"_id":"1"}'))
372
+
373
+ article = PersistentArticle.create :title => 'Test'
374
+ assert_equal '1', article.id
375
+ end
376
+
377
+ should "not set the id property if already set" do
378
+ Configuration.client.expects(:post).
379
+ with do |url, payload|
380
+ doc = MultiJson.decode(payload)
381
+ url == "#{Configuration.url}/persistent_articles/persistent_article/123" &&
382
+ doc['title'] == 'Test' &&
383
+ doc['published_on'] == nil
384
+ end.
385
+ returns(mock_response('{"ok":true, "_id":"XXX"}'))
386
+
387
+ article = PersistentArticle.create :id => '123', :title => 'Test'
388
+ assert_equal '123', article.id
389
+ end
390
+
391
+ end
392
+
393
+ context "when saving" do
394
+
395
+ should "save the document with updated attribute" do
396
+ article = PersistentArticle.new :id => '1', :title => 'Test'
397
+
398
+ Configuration.client.expects(:post).
399
+ with do |url, payload|
400
+ doc = MultiJson.decode(payload)
401
+ url == "#{Configuration.url}/persistent_articles/persistent_article/1" &&
402
+ doc['title'] == 'Test' &&
403
+ doc['published_on'] == nil
404
+ end.
405
+ returns(mock_response('{"ok":true,"_id":"1"}'))
406
+ assert article.save
407
+
408
+ article.title = 'Updated'
409
+
410
+ Configuration.client.expects(:post).
411
+ with do |url, payload|
412
+ doc = MultiJson.decode(payload)
413
+ url == "#{Configuration.url}/persistent_articles/persistent_article/1" &&
414
+ doc['title'] == 'Updated'
415
+ end.
416
+ returns(mock_response('{"ok":true,"_id":"1"}'))
417
+ assert article.save
418
+ end
419
+
420
+ should "perform validations" do
421
+ article = ValidatedModel.new :name => nil
422
+ assert ! article.save
423
+ end
424
+
425
+ should "set the id property itself" do
426
+ article = PersistentArticle.new
427
+ article.title = 'Test'
428
+
429
+ Configuration.client.expects(:post).with("#{Configuration.url}/persistent_articles/persistent_article/",
430
+ article.to_indexed_json).
431
+ returns(mock_response('{"ok":true,"_id":"1"}'))
432
+ assert article.save
433
+ assert_equal '1', article.id
434
+ end
435
+
436
+ should "not set the id property if already set" do
437
+ article = PersistentArticle.new
438
+ article.id = '456'
439
+ article.title = 'Test'
440
+
441
+ Configuration.client.expects(:post).
442
+ with do |url, payload|
443
+ doc = MultiJson.decode(payload)
444
+ url == "#{Configuration.url}/persistent_articles/persistent_article/456" &&
445
+ doc['title'] == 'Test'
446
+ end.
447
+ returns(mock_response('{"ok":true,"_id":"XXX"}'))
448
+ assert article.save
449
+ assert_equal '456', article.id
450
+ end
451
+
452
+ end
453
+
454
+ context "when destroying" do
455
+
456
+ should "delete the document from the database" do
457
+ Configuration.client.expects(:post).
458
+ with do |url, payload|
459
+ doc = MultiJson.decode(payload)
460
+ url == "#{Configuration.url}/persistent_articles/persistent_article/123" &&
461
+ doc['title'] == 'Test'
462
+ end.returns(mock_response('{"ok":true,"_id":"123"}'))
463
+
464
+ Configuration.client.expects(:delete).
465
+ with("#{Configuration.url}/persistent_articles/persistent_article/123").
466
+ returns(mock_response('{"ok":true,"acknowledged":true}', 200))
467
+
468
+ article = PersistentArticle.new :id => '123', :title => 'Test'
469
+ article.save
470
+ article.destroy
471
+ end
472
+
473
+ end
474
+
475
+ context "when updating attributes" do
476
+
477
+ should "update single attribute" do
478
+ @article.expects(:save).returns(true)
479
+
480
+ @article.update_attribute :title, 'Updated'
481
+ assert_equal 'Updated', @article.title
482
+ end
483
+
484
+ should "update all attributes" do
485
+ @article.expects(:save).returns(true)
486
+
487
+ @article.update_attributes :title => 'Updated', :tags => ['three']
488
+ assert_equal 'Updated', @article.title
489
+ assert_equal ['three'], @article.tags
490
+ end
491
+
492
+ end
493
+
494
+ end
495
+
496
+ context "Persistent model with mapping definition" do
497
+
498
+ should "create the index with mapping" do
499
+ expected = {
500
+ :settings => {},
501
+ :mappings => { :persistent_article_with_mapping => {
502
+ :properties => { :title => { :type => 'string', :analyzer => 'snowball', :boost => 10 } }
503
+ }}
504
+ }
505
+
506
+ Tire::Index.any_instance.stubs(:exists?).returns(false)
507
+ Tire::Index.any_instance.expects(:create).with(expected)
508
+
509
+ class ::PersistentArticleWithMapping
510
+
511
+ include Tire::Model::Persistence
512
+ include Tire::Model::Search
513
+ include Tire::Model::Callbacks
514
+
515
+ mapping do
516
+ property :title, :type => 'string', :analyzer => 'snowball', :boost => 10
517
+ end
518
+
519
+ end
520
+
521
+ assert_equal 'snowball', PersistentArticleWithMapping.mapping[:title][:analyzer]
522
+ end
523
+
524
+ end
525
+
526
+ end
527
+ end
528
+ end