elastic_queue 0.0.10 → 0.0.11

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 3b98fbf9af561bf1f2bdaf418941c235e9efb6d5
4
- data.tar.gz: 2a2b8517d4cdb107ad9fdb55ace575741ee5854f
3
+ metadata.gz: 5f6a30f14c2ff7bb0330c854ea5653d295d4f24b
4
+ data.tar.gz: 6aeca9a70bf31dc9b65b0c9862dc835c45535ca2
5
5
  SHA512:
6
- metadata.gz: 6ebcfd62bf72d135e4fa2532d61237996698a8974b99cf2bdfb9293a6705c0f52dc23af9aa6b2e6e46f861d49a711699c5c9b753b9b96c45c72e2c6c5ddcb035
7
- data.tar.gz: e7a20b9650fbd4e411e6449d8373a32416c9a9af9257aeec5caa16827a4faa16985d072d132ba7e5be25893a5d104debace1d5385e2fbf7d684961116b4e59f8
6
+ metadata.gz: 71487d775f4a86d6f9f17abb25173d671b7a153769bdf906e4bd100deede733efec86c85235388e84954a39239448d3afdef7d5c9d5a801dcbd48ab0f47ff990
7
+ data.tar.gz: 4e17e151ca7cb3885eea1f496e283d5cd0f4c6532b1d56d67c34469bd4205875eb071c1322c3bc712ba6ca7e1d36e86e9b8bd8fc8269ea586b369c6da007641d
data/.gitignore CHANGED
@@ -17,3 +17,4 @@ tmp
17
17
  .yardoc
18
18
  _yardoc
19
19
  doc/
20
+ spec/eq_test.db
data/Gemfile.lock CHANGED
@@ -1,7 +1,8 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- elastic_queue (0.0.10)
4
+ elastic_queue (0.0.11)
5
+ activerecord (~> 3.0)
5
6
  activesupport (~> 3.0)
6
7
  elasticsearch (~> 0.4)
7
8
  will_paginate (~> 3.0)
@@ -9,9 +10,26 @@ PATH
9
10
  GEM
10
11
  remote: https://rubygems.org/
11
12
  specs:
12
- activesupport (3.2.17)
13
+ activemodel (3.2.18)
14
+ activesupport (= 3.2.18)
15
+ builder (~> 3.0.0)
16
+ activerecord (3.2.18)
17
+ activemodel (= 3.2.18)
18
+ activesupport (= 3.2.18)
19
+ arel (~> 3.0.2)
20
+ tzinfo (~> 0.3.29)
21
+ activesupport (3.2.18)
13
22
  i18n (~> 0.6, >= 0.6.4)
14
23
  multi_json (~> 1.0)
24
+ arel (3.0.3)
25
+ builder (3.0.4)
26
+ columnize (0.8.9)
27
+ debugger (1.6.6)
28
+ columnize (>= 0.3.1)
29
+ debugger-linecache (~> 1.2.0)
30
+ debugger-ruby_core_source (~> 1.3.2)
31
+ debugger-linecache (1.2.0)
32
+ debugger-ruby_core_source (1.3.2)
15
33
  diff-lcs (1.2.5)
16
34
  elasticsearch (0.4.11)
17
35
  elasticsearch-api (= 0.4.11)
@@ -26,7 +44,7 @@ GEM
26
44
  faraday (0.9.0)
27
45
  multipart-post (>= 1.2, < 3)
28
46
  i18n (0.6.9)
29
- multi_json (1.9.2)
47
+ multi_json (1.10.0)
30
48
  multipart-post (2.0.0)
31
49
  rake (10.3.1)
32
50
  rspec (2.14.1)
@@ -38,15 +56,18 @@ GEM
38
56
  diff-lcs (>= 1.1.3, < 2.0)
39
57
  rspec-mocks (2.14.6)
40
58
  sqlite3 (1.3.9)
59
+ tzinfo (0.3.39)
41
60
  will_paginate (3.0.5)
42
61
 
43
62
  PLATFORMS
44
63
  ruby
45
64
 
46
65
  DEPENDENCIES
66
+ activesupport (~> 3.0)
47
67
  bundler (~> 1.0)
68
+ debugger (~> 1.6)
48
69
  elastic_queue!
49
70
  factory_girl (~> 4.0)
50
71
  rake (~> 10.0)
51
72
  rspec (~> 2.6)
52
- sqlite3
73
+ sqlite3 (~> 1.3)
@@ -19,6 +19,7 @@ Gem::Specification.new do |s|
19
19
  s.test_files = Dir['spec/**/*.rb']
20
20
 
21
21
  s.add_runtime_dependency 'activesupport', '~> 3.0'
22
+ s.add_runtime_dependency 'activerecord', '~>3.0'
22
23
  s.add_runtime_dependency 'elasticsearch', '~> 0.4'
23
24
  s.add_runtime_dependency 'will_paginate', '~> 3.0'
24
25
  s.add_development_dependency 'bundler', '~> 1.0'
@@ -26,4 +27,5 @@ Gem::Specification.new do |s|
26
27
  s.add_development_dependency 'factory_girl', '~> 4.0'
27
28
  s.add_development_dependency 'sqlite3', '~> 1.3'
28
29
  s.add_development_dependency 'rake', '~> 10.0'
29
- end
30
+ s.add_development_dependency 'debugger', '~>1.6'
31
+ end
@@ -9,7 +9,7 @@ module ElasticQueue
9
9
  include Percolation
10
10
 
11
11
  def self.search_client
12
- Elasticsearch::Client.new hosts: ElasticQueue::OPTIONS[:elasticsearch_hosts]
12
+ @search_client ||= Elasticsearch::Client.new hosts: ElasticQueue::OPTIONS[:elasticsearch_hosts]
13
13
  end
14
14
 
15
15
  def self.models(*models)
@@ -12,18 +12,27 @@ module ElasticQueue
12
12
  private
13
13
 
14
14
  def option_to_filter(key, value)
15
- if value.is_a? Array
15
+ # return and_options(value) if key == :and
16
+ if [:or, :and].include?(key)
17
+ join_options(key, value)
18
+ elsif value.is_a? Array
16
19
  or_filter(key, value)
17
20
  elsif value.is_a? Hash
18
- # date?
19
- time_filter(key, value)
21
+ comparison_filter(key, value)
20
22
  elsif value.nil?
23
+ # e.g. name: nil
21
24
  null_filter(key, value)
22
25
  else
26
+ # e.g. status: 'fresh'
23
27
  term_filter(key, value)
24
28
  end
25
29
  end
26
30
 
31
+ def join_options(operator, options)
32
+ conditions = options.map { |o| options_to_filters(o) }.flatten
33
+ { operator => conditions }
34
+ end
35
+
27
36
  def or_filter(term, values)
28
37
  # flatten here because ranges return arrays
29
38
  conditions = values.map { |v| option_to_filter(term, v) }.flatten
@@ -35,7 +44,7 @@ module ElasticQueue
35
44
  end
36
45
 
37
46
  # take something like follow_up: { before: 'hii', after: 'low' }
38
- def time_filter(term, value)
47
+ def comparison_filter(term, value)
39
48
  value.map do |k, v|
40
49
  comparator = k.to_sym.in?([:after, :greater_than, :gt]) ? :gt : :lt
41
50
  range_filter(term, v, comparator)
@@ -13,7 +13,7 @@ module ElasticQueue
13
13
  end
14
14
 
15
15
  def create_index
16
- search_client.indices.create index: index_name
16
+ search_client.indices.create index: index_name, body: default_index_settings
17
17
  add_mappings
18
18
  end
19
19
 
@@ -26,12 +26,14 @@ module ElasticQueue
26
26
  search_client.indices.refresh index: index_name
27
27
  end
28
28
 
29
- def bulk_index(batch_size = 10_000)
29
+ # you can pass scopes into bulk_index to be used when fetching records
30
+ # bulk_index(scopes: { some_model: [:scope1, :scope2], some_other_model: [:scope3] }) will fetch SomeModel.scope1.scope2 and SomeOtherModel.scope3 and index only those records.
31
+ def bulk_index(scopes: {}, batch_size: 10_000)
30
32
  create_index unless index_exists?
31
33
  model_classes.each do |klass|
32
34
  # modelclass(model).includes(associations_for_index(model)).
33
35
  index_type = klass.to_s.underscore
34
- klass.find_in_batches(batch_size: batch_size) do |batch|
36
+ scoped_class(klass, scopes).find_in_batches(batch_size: batch_size) do |batch|
35
37
  body = []
36
38
  batch.each do |instance|
37
39
  body << { index: { _index: index_name, _id: instance.id, _type: index_type, data: instance.indexed_for_queue } }
@@ -41,9 +43,34 @@ module ElasticQueue
41
43
  end
42
44
  end
43
45
 
46
+ def scoped_class(klass, scopes)
47
+ return klass unless scopes[klass.to_s.underscore.to_sym]
48
+ scopes[klass.to_s.underscore.to_sym].each do |scope|
49
+ klass = klass.send(scope)
50
+ end
51
+ klass
52
+ end
53
+
54
+ def default_index_settings
55
+ {
56
+ settings: {
57
+ analysis: {
58
+ analyzer: {
59
+ default: {
60
+ type: :custom,
61
+ tokenizer: :whitespace,
62
+ filter: [:lowercase]
63
+ }
64
+ }
65
+ }
66
+ }
67
+ }
68
+ end
69
+
44
70
  def add_mappings
45
71
  model_classes.each do |klass|
46
- search_client.indices.put_mapping index: index_name, type: klass.to_s.underscore, body: klass.queue_mapping
72
+ mapping = klass.queue_mapping
73
+ search_client.indices.put_mapping index: index_name, type: klass.to_s.underscore, body: mapping if mapping.present?
47
74
  end
48
75
  end
49
76
 
@@ -45,40 +45,39 @@ module ElasticQueue
45
45
 
46
46
  def paginate(options = {})
47
47
  options.each { |k, v| @options.send("#{k}=", v) }
48
- all.paginate
48
+ Results.new(@queue, execute(paginate: true), @options).paginate
49
49
  end
50
50
 
51
+ # TODO: remove if not using, add per_page if using
51
52
  def page=(page)
52
53
  @options.page = (page)
53
54
  end
54
55
 
55
56
  def all
56
- @results ||= Results.new(@queue, execute, @options)
57
+ Results.new(@queue, execute, @options).instantiated_queue_items
57
58
  end
58
59
 
59
60
  # return just the ids of the records (useful when combined with SQL queries)
60
61
  def ids
61
- results = execute
62
- results[:hits][:hits].map { |h| h[:_source][:id] }
62
+ execute[:hits][:hits].map { |h| h[:_source][:id] }
63
63
  end
64
64
 
65
65
  def count
66
- res = execute(count: true)
67
- res[:hits][:total].to_i
66
+ execute(count: true, paginate: false)[:hits][:total].to_i
68
67
  end
69
68
 
70
- def execute(count: false)
69
+ private
70
+
71
+ def execute(count: false, paginate: false)
71
72
  begin
72
- search = execute_query(count: false)
73
- search = substitute_page(search) if !count && search['hits']['hits'].length == 0 && search['hits']['total'] != 0
73
+ search = paginate ? execute_paginated_query : execute_all_query( count: count )
74
+ search = substitute_page(search) if paginate && !count && search['hits']['hits'].length == 0 && search['hits']['total'] != 0
74
75
  rescue Elasticsearch::Transport::Transport::Errors::BadRequest
75
76
  search = failed_search
76
77
  end
77
78
  search.with_indifferent_access
78
79
  end
79
80
 
80
- private
81
-
82
81
  # this allows you to chain scopes
83
82
  # the 2+ scopes in the chain will be called
84
83
  # on a query object and not on the base object
@@ -89,16 +88,21 @@ module ElasticQueue
89
88
  end
90
89
  end
91
90
 
92
- def execute_query(count: false)
93
- search_type = count ? 'count' : 'query_then_fetch'
94
- @queue.search_client.search index: @queue.index_name, body: body, search_type: search_type, from: @options.from, size: @options.per_page
91
+ def execute_all_query(count: false)
92
+ record_count = @queue.search_client.search index: @queue.index_name, body: body, search_type: 'count'
93
+ return record_count if count
94
+ @queue.search_client.search index: @queue.index_name, body: body, search_type: 'query_then_fetch', from: 0, size: record_count['hits']['total'].to_i
95
+ end
96
+
97
+ def execute_paginated_query
98
+ @queue.search_client.search index: @queue.index_name, body: body, search_type: 'query_then_fetch', from: @options.from, size: @options.per_page
95
99
  end
96
100
 
97
101
  def substitute_page(search)
98
102
  total_hits = search['hits']['total'].to_i
99
103
  per_page = @options.per_page
100
104
  @options.page = (total_hits / per_page.to_f).ceil
101
- execute_query
105
+ execute_paginated_query
102
106
  end
103
107
 
104
108
  def failed_search
@@ -9,6 +9,7 @@ module ElasticQueue
9
9
  end
10
10
 
11
11
  module ClassMethods
12
+
12
13
  def queues(*queues)
13
14
  @queues ||= queues
14
15
  end
@@ -2,9 +2,8 @@ require 'will_paginate/collection'
2
2
 
3
3
  module ElasticQueue
4
4
  class Results
5
- attr_reader :paginate
6
5
 
7
- delegate :empty?, :each, :total_entries, :total_pages, :current_page, to: :paginate
6
+ attr_reader :instantiated_queue_items
8
7
 
9
8
  def initialize(queue, search_results, query_options)
10
9
  @queue = queue
@@ -12,19 +11,22 @@ module ElasticQueue
12
11
  @start = query_options.page
13
12
  @per_page = query_options.per_page
14
13
  @total = search_results[:hits][:total]
15
- @paginate = WillPaginate::Collection.create(@start, @per_page, @total) do |pager|
14
+ end
15
+
16
+ def paginate
17
+ WillPaginate::Collection.create(@start, @per_page, @total) do |pager|
16
18
  pager.replace(@instantiated_queue_items)
17
19
  end
18
20
  end
19
21
 
22
+ private
23
+
20
24
  def instantiate_queue_items(search_results)
21
25
  grouped_results, sort_order = group_sorted_results(search_results)
22
26
  records = fetch_records(grouped_results)
23
27
  sort_records(records, sort_order)
24
28
  end
25
29
 
26
- private
27
-
28
30
  # group the results by { class_name: [ids] } and save their sorted order
29
31
  def group_sorted_results(search_results)
30
32
  grouped_results = {}
@@ -1,3 +1,3 @@
1
1
  module ElasticQueue
2
- VERSION = '0.0.10'
2
+ VERSION = '0.0.11'
3
3
  end
data/spec/factories.rb CHANGED
@@ -1,64 +0,0 @@
1
- FactoryGirl.define do
2
- sequence :email_address do |n|
3
- "testy#{n}@example.com"
4
- end
5
-
6
- sequence :user_id do
7
- (10000...10005).to_a.sample
8
- end
9
-
10
- sequence :agent_fee_sales_session_status do
11
- ['active', 'dead'].sample
12
- end
13
-
14
- factory :agent_fee_sales_session do
15
- agent
16
- status { generate(:agent_fee_sales_session_status) }
17
- assigned_to { generate(:user_id) }
18
- assigned_at Time.parse('2013-12-02 08:40:33')
19
- expires_at Time.parse('2014-06-02 08:40:33')
20
- follow_up Time.parse('2013-12-17 06:00:00')
21
- hot '1'
22
- priority 'high'
23
- created_at Time.parse('2013-11-18 14:36:05')
24
- updated_at Time.parse('2013-12-17 11:31:08')
25
- factory :null_follow_up_agent_fee_sales_session do
26
- follow_up nil
27
- end
28
- end
29
-
30
- factory :agent do
31
- after(:build) { |agent| agent.class.skip_callback(:save, :after, :notate_changes) }
32
- user
33
- company
34
- status 'active'
35
- name_on_license 'Testy'
36
- license_type 'Salesperson'
37
- license_number '111111'
38
- license_state 'CA'
39
- broker_name 'Ali'
40
- years_in_real_estate '12 years'
41
- end
42
-
43
- factory :company do
44
- name 'Flywheel'
45
- address '233 Post st.'
46
- city 'San Francisco'
47
- state 'CA'
48
- zip '94104'
49
- end
50
-
51
- factory :user do
52
- after(:build) { |user| user.class.skip_callback(:save, :after, :notate_changes) }
53
- email { generate(:email_address) }
54
- login { |u| u.email }
55
- password '1234567'
56
- first_name 'Testy'
57
- last_name 'Testerson'
58
- phone_office '415-555-5555'
59
- factory :agent_user do
60
- user_type 'agent'
61
- end
62
- end
63
-
64
- end
@@ -0,0 +1,70 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ElasticQueue::Base integration' do
4
+ describe '#search_client' do
5
+ it 'returns an Elasticsearch::Transport::Client' do
6
+ expect(ElasticQueue::Base.search_client).to be_a Elasticsearch::Transport::Client
7
+ end
8
+
9
+ it 'returns the same client every time' do
10
+ expect(ElasticQueue::Base.search_client.object_id).to eq ElasticQueue::Base.search_client.object_id
11
+ end
12
+ end
13
+
14
+ describe '#models, also tests(#tell_models, #model_names, #model_classes)' do
15
+ it '#models sets @models and tells the model about itself' do
16
+ class Cannibal < ActiveRecord::Base
17
+ include ElasticQueue::Queueable
18
+ end
19
+ # Cannibal.stub(:add_queue)
20
+ # Cannibal.should_receive(:add_queue).with(:"elastic_queue/base")
21
+ ElasticQueue::Base.models(:cannibal)
22
+ expect(ElasticQueue::Base.instance_variable_get('@models')).to eq [:cannibal]
23
+ end
24
+ end
25
+
26
+ describe '#index_name, #index_name =' do
27
+ pending('trivial')
28
+ end
29
+
30
+ describe '#eager_load' do
31
+ pending
32
+ end
33
+
34
+ describe '#eager_loads' do
35
+ pending
36
+ end
37
+
38
+ describe '#scopes' do
39
+ pending
40
+ end
41
+
42
+ describe '#scopes' do
43
+ pending
44
+ end
45
+
46
+ describe '#default_scope' do
47
+ pending
48
+ end
49
+
50
+ describe '#query' do
51
+ pending
52
+ end
53
+
54
+ describe '#filter' do
55
+ pending
56
+ end
57
+
58
+ describe '#count' do
59
+ pending
60
+ end
61
+
62
+ describe '#paginate' do
63
+ pending('not implemented yet')
64
+ end
65
+
66
+ describe 'instance #query' do
67
+ pending
68
+ end
69
+
70
+ end
@@ -0,0 +1,123 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'ElasticQueue::Filters integration' do
4
+ before :all do
5
+ class Animal < ActiveRecord::Base
6
+ include ElasticQueue::Queueable
7
+ queues :test_animals_queue
8
+ queue_attributes :dangerous, :cute, :birthdate, :name
9
+ not_analyzed_queue_attributes :species, :description
10
+ end
11
+
12
+ class TestAnimalsQueue < ElasticQueue::Base
13
+ models :animal
14
+ end
15
+
16
+ TestAnimalsQueue.create_index
17
+
18
+ @create_animals = -> {
19
+ Animal.create({ name: 'a', birthdate: Date.today.at_midnight - 1.year })
20
+ Animal.create({ name: 'b', birthdate: Date.today.at_midnight - 2.years })
21
+ Animal.create({ name: 'c', birthdate: Date.today.at_midnight - 3.years })
22
+ }
23
+ end
24
+
25
+ after :all do
26
+ [:Animal, :TestAnimalsQueue].each do |constant|
27
+ Object.send(:remove_const, constant)
28
+ end
29
+ delete_index('test_animals_queue')
30
+ end
31
+
32
+ describe 'ElasticQueue::Query#filter' do
33
+ after :each do
34
+ Animal.all.each(&:destroy)
35
+ end
36
+
37
+ it 'can filter on one value' do
38
+ @create_animals.call
39
+ expect(TestAnimalsQueue.query.filter(name: 'a').all.map(&:name)).to eq ['a']
40
+ end
41
+
42
+ it 'can filter by a less than or greater than a time' do
43
+ @create_animals.call
44
+ expect(TestAnimalsQueue.query.filter(birthdate: { after: Date.today - 1.year - 1.day }).all.map(&:name)).to eq ['a']
45
+ expect(TestAnimalsQueue.query.filter(birthdate: { before: Date.today - 2.years - 1.day }).all.map(&:name)).to eq ['c']
46
+ end
47
+
48
+ it 'can filter by a less than and greater than a time' do
49
+ @create_animals.call
50
+ expect(TestAnimalsQueue.query.filter(birthdate: { after: Date.today - 2.year - 1.day, before: Date.today - 1.year - 1.day}).all.map(&:name)).to eq ['b']
51
+ end
52
+
53
+ it 'can filter by less than or greater than a string' do
54
+ @create_animals.call
55
+ expect(TestAnimalsQueue.query.filter(name: { after: 'a', before: 'c' }).all.map(&:name)).to eq ['b']
56
+ end
57
+
58
+ it 'doesn\'t error if you try to filter on an nonexistent value' do
59
+ @create_animals.call
60
+ expect(TestAnimalsQueue.query.filter(likes_peanut_butter: true).all.map(&:name)).to eq []
61
+ end
62
+
63
+ it 'filters underscored values as one word' do
64
+ Animal.create({ name: 'pin_head' })
65
+ Animal.create({ name: 'pin' })
66
+ expect(TestAnimalsQueue.query.filter(name: 'pin').all.map(&:name)).to eq ['pin']
67
+ end
68
+
69
+ it 'treats parentheses as letters' do
70
+ Animal.create({ name: 'Sr. Honks-a-lot' , species: '(Silly) Goose' })
71
+ expect(TestAnimalsQueue.query.filter(species: 'Goose').all.map(&:name)).to eq []
72
+ expect(TestAnimalsQueue.query.filter(species: '(Silly) Goose').all.map(&:name)).to eq ['Sr. Honks-a-lot']
73
+ end
74
+
75
+ it 'automatically joins multiple filter values with an OR' do
76
+ @create_animals.call
77
+ expect(TestAnimalsQueue.query.filter(name: ['a', 'b']).all.map(&:name).sort).to eq ['a', 'b']
78
+ end
79
+
80
+ it 'defaults to joining multiple filter keys with an AND' do
81
+ Animal.create({ name: 'x', species: 'dog' })
82
+ Animal.create({ name: 'y', species: 'dog' })
83
+ expect(TestAnimalsQueue.query.filter(name: 'x').filter(species: 'dog').all.map(&:name)).to eq ['x']
84
+ end
85
+
86
+ it 'can join multiple filter keys with an OR' do
87
+ Animal.create({ name: 'x', species: 'dog' })
88
+ Animal.create({ name: 'y', species: 'cat' })
89
+ Animal.create({ name: 'z', species: 'rat' })
90
+ expect(TestAnimalsQueue.query.filter(or: [{ name: 'x' }, { species: 'cat' }]).all.map(&:name).sort).to eq ['x', 'y']
91
+ end
92
+
93
+ it 'can join multiple filter values with multiple filter keys with an OR' do
94
+ Animal.create({ name: 'x', species: 'dog' })
95
+ Animal.create({ name: 'y', species: 'cat' })
96
+ Animal.create({ name: 'z', species: 'chicken' })
97
+ Animal.create({ name: 'a', species: 'chicken' })
98
+ expect(TestAnimalsQueue.query.filter(or: [{ name: 'z' }, { species: ['cat', 'dog'] }]).all.map(&:name).sort).to eq ['x', 'y', 'z']
99
+ end
100
+
101
+ it 'can nest multiple ands and ors' do
102
+ Animal.create({ name: 'rusty', species: 'dog', dangerous: false })
103
+ Animal.create({ name: 'killer', species: 'mountain lion', dangerous: true })
104
+ Animal.create({ name: 'cock-a-doodle-doo', species: 'chicken', dangerous: false })
105
+ Animal.create({ name: 'old bess', species: 'cow', dangerous: true })
106
+ Animal.create({ name: 'speedy', species: 'horse', dangerous: false })
107
+ expect(TestAnimalsQueue.query.filter({
108
+ # (rusty, killer, speedy) && ( rusty || lucky, killer, old bess, cock-a-doodle-doo)
109
+ name: ['rusty', 'killer', 'speedy'], #(rusty, killer, speedy) && (
110
+ or: [
111
+ and: [ # rusty ||
112
+ { species: ['dog', 'mountain lion'] },
113
+ { dangerous: false }
114
+ ],
115
+ or: [ #speedy, killer, old bess, cock-a-doodle-doo )
116
+ { species: ['horse', 'chicken'] },
117
+ { dangerous: true }
118
+ ]
119
+ ]
120
+ }).all.map(&:name).sort).to eq ['killer', 'rusty', 'speedy']
121
+ end
122
+ end
123
+ end