tire-erez 0.5.4

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