load_balanced_tire 0.1

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