mongoid-casino 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +20 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +22 -0
- data/README.md +93 -0
- data/Rakefile +9 -0
- data/casino.gemspec +30 -0
- data/lib/casino.rb +13 -0
- data/lib/casino/collection.rb +195 -0
- data/lib/casino/dimension.rb +11 -0
- data/lib/casino/focus.rb +26 -0
- data/lib/casino/intersection.rb +30 -0
- data/lib/casino/intersection/match/all.rb +29 -0
- data/lib/casino/intersection/match/base.rb +49 -0
- data/lib/casino/intersection/match/equivalence.rb +25 -0
- data/lib/casino/intersection/match/expression.rb +15 -0
- data/lib/casino/intersection/match/greater.rb +43 -0
- data/lib/casino/intersection/match/include.rb +15 -0
- data/lib/casino/intersection/match/lesser.rb +31 -0
- data/lib/casino/intersection/match/recurse.rb +19 -0
- data/lib/casino/lobby.rb +21 -0
- data/lib/casino/projection.rb +76 -0
- data/lib/casino/query.rb +23 -0
- data/lib/casino/question.rb +9 -0
- data/lib/casino/store.rb +49 -0
- data/lib/casino/version.rb +3 -0
- data/spec/fabricators/model_fabricator.rb +12 -0
- data/spec/fabricators/note_fabricator.rb +3 -0
- data/spec/fixtures/collections/collection.rb +3 -0
- data/spec/fixtures/collections/emails_by_day.rb +28 -0
- data/spec/fixtures/models/model.rb +8 -0
- data/spec/fixtures/models/note.rb +7 -0
- data/spec/lib/casino/collection_spec.rb +146 -0
- data/spec/lib/casino/dimension_spec.rb +27 -0
- data/spec/lib/casino/focus_spec.rb +53 -0
- data/spec/lib/casino/intersection/match/all_spec.rb +24 -0
- data/spec/lib/casino/intersection/match/base_spec.rb +159 -0
- data/spec/lib/casino/intersection/match/equivalence_spec.rb +26 -0
- data/spec/lib/casino/intersection/match/expression_spec.rb +26 -0
- data/spec/lib/casino/intersection/match/greater_spec.rb +72 -0
- data/spec/lib/casino/intersection/match/include_spec.rb +27 -0
- data/spec/lib/casino/intersection/match/lesser_spec.rb +74 -0
- data/spec/lib/casino/intersection/match/recurse_spec.rb +24 -0
- data/spec/lib/casino/intersection_spec.rb +33 -0
- data/spec/lib/casino/lobby_spec.rb +20 -0
- data/spec/lib/casino/projection_spec.rb +81 -0
- data/spec/lib/casino/query_spec.rb +42 -0
- data/spec/lib/casino/question_spec.rb +10 -0
- data/spec/lib/casino/store_spec.rb +60 -0
- data/spec/spec_helper.rb +26 -0
- data/spec/support/mongoid.yml +6 -0
- metadata +234 -0
@@ -0,0 +1,28 @@
|
|
1
|
+
class EmailsByDay < Collection
|
2
|
+
|
3
|
+
focus Model
|
4
|
+
|
5
|
+
dimension "Date", :created_at, :last_seven_days
|
6
|
+
dimension "Source", :source, :primary_sources
|
7
|
+
|
8
|
+
question :unique_emails, :count_emails
|
9
|
+
|
10
|
+
def last_seven_days
|
11
|
+
(7.days.ago.to_date..Date.today).map do |date|
|
12
|
+
key = date.strftime("%m/%d/%Y")
|
13
|
+
range = date.beginning_of_day
|
14
|
+
query(key, range)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
def primary_sources
|
19
|
+
%w(Facebook Google Twitter).map do |source|
|
20
|
+
query(source, source)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
def count_emails
|
25
|
+
intersection.distinct(:email).count
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
@@ -0,0 +1,146 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Casino::Collection do
|
4
|
+
describe "making a Casino collection" do
|
5
|
+
it { Collection.must_respond_to :dimension }
|
6
|
+
it { Collection.must_respond_to :focus }
|
7
|
+
it { Collection.must_respond_to :question }
|
8
|
+
it { Collection.must_respond_to :lobby }
|
9
|
+
it { Collection.must_respond_to :register }
|
10
|
+
it { Collection.new.must_respond_to :query }
|
11
|
+
it { Collection.new.must_respond_to :intersection }
|
12
|
+
it { Collection.new.must_respond_to :answer }
|
13
|
+
it { Collection.new.must_respond_to :projection }
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '.dimension' do
|
17
|
+
let(:field) { :created_at }
|
18
|
+
let(:queries) { [] }
|
19
|
+
let(:approach) { { operator: :and } }
|
20
|
+
subject { Collection.dimension(field, queries, approach) }
|
21
|
+
it { subject.must_be_instance_of Casino::Dimension }
|
22
|
+
it "registers the dimension with the Lobby" do
|
23
|
+
dimension = Collection.dimension(field, queries, approach)
|
24
|
+
Collection.lobby.registry[:dimension].must_include dimension
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe '.focus' do
|
29
|
+
let(:model) { Model }
|
30
|
+
let(:focus) { Collection.focus(model) }
|
31
|
+
subject { focus }
|
32
|
+
it { subject.must_be_instance_of Casino::Focus }
|
33
|
+
it "registers the focus with the Lobby" do
|
34
|
+
focus = Collection.focus(Model)
|
35
|
+
Collection.lobby.registry[:focus].first.must_equal focus
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe '.question' do
|
40
|
+
let(:name) { :emails }
|
41
|
+
let(:answer) { -> { distinct(:email) } }
|
42
|
+
subject { Collection.question(name, answer) }
|
43
|
+
it { subject.must_be_instance_of Casino::Question }
|
44
|
+
it "registers the question with the Lobby" do
|
45
|
+
question = Collection.question(name, answer)
|
46
|
+
Collection.lobby.registry[:question].must_include question
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
describe 'instance methods' do
|
51
|
+
|
52
|
+
let(:emails_by_day) { EmailsByDay.new }
|
53
|
+
|
54
|
+
describe 'registry accessors' do
|
55
|
+
|
56
|
+
let(:registry) { emails_by_day.class.lobby.registry }
|
57
|
+
let(:dimensions) { registry[:dimension] }
|
58
|
+
let(:questions) { registry[:question] }
|
59
|
+
let(:focus) { registry[:focus] }
|
60
|
+
|
61
|
+
subject { emails_by_day }
|
62
|
+
|
63
|
+
it { subject.dimensions.must_equal dimensions }
|
64
|
+
it { subject.focus.must_equal focus }
|
65
|
+
it { subject.questions.must_equal questions }
|
66
|
+
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '#query' do
|
70
|
+
subject { Collection.new.query('', '', '') }
|
71
|
+
it { subject.must_be_instance_of Casino::Query }
|
72
|
+
end
|
73
|
+
|
74
|
+
describe '#store' do
|
75
|
+
subject { emails_by_day.store }
|
76
|
+
it { subject.must_be_instance_of Casino::Store }
|
77
|
+
end
|
78
|
+
|
79
|
+
describe '#results' do
|
80
|
+
subject { emails_by_day.results }
|
81
|
+
it { subject.must_be_instance_of Mongoid::Criteria }
|
82
|
+
end
|
83
|
+
|
84
|
+
describe '#update' do
|
85
|
+
subject { emails_by_day.update }
|
86
|
+
it { subject.must_be_instance_of Mongoid::Criteria }
|
87
|
+
end
|
88
|
+
|
89
|
+
describe '#insert' do
|
90
|
+
subject { emails_by_day.insert(Model.new) }
|
91
|
+
it "returns a `Mongoid::Criteria`" do
|
92
|
+
subject.must_be_instance_of Mongoid::Criteria
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#projection' do
|
97
|
+
|
98
|
+
let(:selector) { Model.where(created_at: Time.now).selector }
|
99
|
+
let(:mock) { OpenStruct.new(selector: selector) }
|
100
|
+
|
101
|
+
subject { emails_by_day.projection }
|
102
|
+
|
103
|
+
it { subject.must_be_instance_of Casino::Projection }
|
104
|
+
|
105
|
+
it "is preloaded with the current criteria intersection" do
|
106
|
+
emails_by_day.stub(:intersection, mock) do
|
107
|
+
subject.pipeline.first['$match'].must_equal selector
|
108
|
+
end
|
109
|
+
end
|
110
|
+
|
111
|
+
end
|
112
|
+
|
113
|
+
describe '#intersection' do
|
114
|
+
subject { emails_by_day.intersection }
|
115
|
+
it { subject.must_be_instance_of Mongoid::Criteria }
|
116
|
+
it "defaults to the base model criteria" do
|
117
|
+
subject.must_equal Model.scoped
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe '#answer' do
|
122
|
+
subject { emails_by_day.answer(:count_emails) }
|
123
|
+
it "calls the target method" do
|
124
|
+
emails_by_day.stub(:count_emails, true) do
|
125
|
+
subject.must_equal true
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
describe '#key' do
|
131
|
+
|
132
|
+
let(:string) do
|
133
|
+
"emails_by_day_asks_model_about_unique_emails" +
|
134
|
+
"_by_created_at_and_source"
|
135
|
+
end
|
136
|
+
|
137
|
+
let(:key) { Digest::SHA1.hexdigest(string) }
|
138
|
+
|
139
|
+
subject { emails_by_day.key }
|
140
|
+
it { subject.must_equal key }
|
141
|
+
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Casino::Dimension do
|
4
|
+
|
5
|
+
let(:label) { "Date" }
|
6
|
+
let(:field) { :created_at }
|
7
|
+
let(:queries) { :method_name }
|
8
|
+
let(:and_approach) { { operator: :and } }
|
9
|
+
let(:where_approach) { { operator: :where } }
|
10
|
+
|
11
|
+
let(:dimension) do
|
12
|
+
Casino::Dimension.new(label, field, queries, and_approach)
|
13
|
+
end
|
14
|
+
|
15
|
+
subject { dimension }
|
16
|
+
|
17
|
+
it { subject.label.must_equal label }
|
18
|
+
it { subject.queries.must_equal queries }
|
19
|
+
it { subject.approach.must_equal and_approach }
|
20
|
+
it { subject.field.must_equal field }
|
21
|
+
|
22
|
+
it 'defaults #approach to operator: :where' do
|
23
|
+
dimension = Casino::Dimension.new(label, field, queries)
|
24
|
+
dimension.approach.must_equal where_approach
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Casino::Focus do
|
4
|
+
|
5
|
+
let(:model) { Model }
|
6
|
+
let(:focus) { Casino::Focus.new(model) }
|
7
|
+
|
8
|
+
subject { focus }
|
9
|
+
|
10
|
+
it { subject.model.must_equal model }
|
11
|
+
|
12
|
+
describe '#eql?' do
|
13
|
+
|
14
|
+
let(:match) { Casino::Focus.new(model) }
|
15
|
+
let(:no_match) { Casino::Focus.new(Class.new) }
|
16
|
+
|
17
|
+
it "is true when compared to another focus of the same model" do
|
18
|
+
subject.eql?(match).must_equal true
|
19
|
+
end
|
20
|
+
|
21
|
+
it "is false when compared to a different model focus" do
|
22
|
+
subject.eql?(no_match).must_equal false
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#hash' do
|
27
|
+
it "delegates to the model hash" do
|
28
|
+
subject.hash.must_equal model.hash
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
describe '#build_criteria' do
|
33
|
+
|
34
|
+
let(:conditions) do
|
35
|
+
[ [:where, { created_at: Time.now }],
|
36
|
+
[:and, { source: "Facebook" }] ]
|
37
|
+
end
|
38
|
+
|
39
|
+
let(:query) do
|
40
|
+
Casino::Query.new(:label, conditions)
|
41
|
+
end
|
42
|
+
|
43
|
+
subject { focus.build_criteria(query) }
|
44
|
+
|
45
|
+
it "produces a Mongoid::Criteria with the correct fields" do
|
46
|
+
[ "created_at", "$and" ].each do |field|
|
47
|
+
subject.selector.keys.must_include field
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
|
53
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
describe Casino::Intersection::Match::All do
|
2
|
+
|
3
|
+
let(:system) { Casino::Intersection::Match::All }
|
4
|
+
|
5
|
+
describe '#eligible?' do
|
6
|
+
let(:matcher) { system.new('$and', '', '', '') }
|
7
|
+
subject { matcher.eligible? }
|
8
|
+
it { subject.must_equal true }
|
9
|
+
end
|
10
|
+
|
11
|
+
describe '#evaluate' do
|
12
|
+
|
13
|
+
let(:matcher) { system.new('$and', '', '', [{}]) }
|
14
|
+
let(:base) { Casino::Intersection::Match::Base }
|
15
|
+
subject { matcher.evaluate }
|
16
|
+
it 'builds base matchers to evaluate' do
|
17
|
+
base.stub(:new, OpenStruct.new(evaluate: true)) do
|
18
|
+
subject.must_equal true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
@@ -0,0 +1,159 @@
|
|
1
|
+
# If it's a regular expression, match with =~
|
2
|
+
# If it's a hash, recurse over the hash with the document
|
3
|
+
# If it is in the following directives, do the directive operation:
|
4
|
+
# * $gt evaluates with >
|
5
|
+
# * $gte evaluates with >=
|
6
|
+
# * $lt evalutes with <
|
7
|
+
# * $lte evaluates with <=
|
8
|
+
# * $in evaluates with Array#include?
|
9
|
+
# * $and evalutes as a list with each element of the list
|
10
|
+
# representing a new level of Match::Base with its own
|
11
|
+
# key
|
12
|
+
# If it's a string/integer, match with ==
|
13
|
+
|
14
|
+
describe Casino::Intersection::Match::Base do
|
15
|
+
|
16
|
+
let(:base_class) { Casino::Intersection::Match::Base }
|
17
|
+
let(:document) { Model.new(created_at: 3.days.ago) }
|
18
|
+
let(:key) { 'created_at' }
|
19
|
+
let(:criteria) { Model.gt(created_at: 4.days.ago) }
|
20
|
+
let(:selector) { criteria.selector }
|
21
|
+
let(:value) { document.created_at }
|
22
|
+
let(:base) { base_class.new(document, key, selector, value) }
|
23
|
+
let(:source) { "Facebook" }
|
24
|
+
let(:label) { [source] }
|
25
|
+
let(:intersection) { Casino::Intersection.new(label, criteria) }
|
26
|
+
|
27
|
+
describe 'when the document has an equivalent value' do
|
28
|
+
let(:model) { Model.new(source: source) }
|
29
|
+
let(:criteria) { Model.where(source: source) }
|
30
|
+
subject { intersection.match?(model) }
|
31
|
+
it { subject.must_equal true }
|
32
|
+
end
|
33
|
+
|
34
|
+
describe 'when the document has a regular expression' do
|
35
|
+
let(:model) { Model.new(source: source) }
|
36
|
+
let(:criteria) { Model.where(source: /Face/i) }
|
37
|
+
subject { intersection.match?(model) }
|
38
|
+
it { subject.must_equal true }
|
39
|
+
end
|
40
|
+
|
41
|
+
describe 'directives' do
|
42
|
+
|
43
|
+
describe '$and' do
|
44
|
+
let(:model) { Model.new(source: "Facebook", created_at: 3.days.ago) }
|
45
|
+
|
46
|
+
let(:criteria) do
|
47
|
+
Model.and(:created_at.gt => 4.days.ago).
|
48
|
+
and(source: "Facebook")
|
49
|
+
end
|
50
|
+
|
51
|
+
subject { intersection.match?(model) }
|
52
|
+
it { subject.must_equal true }
|
53
|
+
end
|
54
|
+
|
55
|
+
describe '$in' do
|
56
|
+
let(:model) { Model.new(source: "Facebook") }
|
57
|
+
let(:criteria) { Model.in(source: "Facebook") }
|
58
|
+
subject { intersection.match?(model) }
|
59
|
+
it { subject.must_equal true }
|
60
|
+
end
|
61
|
+
|
62
|
+
describe '$gt' do
|
63
|
+
let(:model) { Model.new(created_at: 3.days.ago) }
|
64
|
+
let(:criteria) { Model.gt(created_at: 4.days.ago) }
|
65
|
+
subject { intersection.match?(model) }
|
66
|
+
it { subject.must_equal true }
|
67
|
+
end
|
68
|
+
|
69
|
+
describe '$lt' do
|
70
|
+
let(:model) { Model.new(created_at: 4.days.ago) }
|
71
|
+
let(:criteria) { Model.lt(created_at: 3.days.ago) }
|
72
|
+
subject { intersection.match?(model) }
|
73
|
+
it { subject.must_equal true }
|
74
|
+
end
|
75
|
+
|
76
|
+
describe '$gte' do
|
77
|
+
let(:model) { Model.new(created_at: 3.days.ago) }
|
78
|
+
let(:criteria) { Model.gte(created_at: 3.days.ago) }
|
79
|
+
subject { intersection.match?(model) }
|
80
|
+
it { subject.must_equal true }
|
81
|
+
end
|
82
|
+
|
83
|
+
describe '$lte' do
|
84
|
+
let(:three_days_ago) { 3.days.ago }
|
85
|
+
let(:model) { Model.new(created_at: three_days_ago) }
|
86
|
+
let(:criteria) { Model.lte(created_at: three_days_ago) }
|
87
|
+
subject { intersection.match?(model) }
|
88
|
+
it { subject.must_equal true }
|
89
|
+
end
|
90
|
+
|
91
|
+
end
|
92
|
+
|
93
|
+
describe 'creating a new matcher base' do
|
94
|
+
|
95
|
+
describe '#document' do
|
96
|
+
subject { base.document }
|
97
|
+
it { subject.must_equal document }
|
98
|
+
end
|
99
|
+
|
100
|
+
describe '#key' do
|
101
|
+
subject { base.key }
|
102
|
+
it { subject.must_equal key }
|
103
|
+
end
|
104
|
+
|
105
|
+
describe '#selector' do
|
106
|
+
subject { base.selector }
|
107
|
+
it { subject.must_equal selector }
|
108
|
+
end
|
109
|
+
|
110
|
+
describe '#field' do
|
111
|
+
subject { base.field }
|
112
|
+
it { subject.must_equal selector[key] }
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#value' do
|
116
|
+
subject { base.value }
|
117
|
+
it { subject.must_equal document.send(key) }
|
118
|
+
end
|
119
|
+
|
120
|
+
end
|
121
|
+
|
122
|
+
describe '#evaluate' do
|
123
|
+
it 'calls #evaluate on the given #matcher' do
|
124
|
+
matcher = MiniTest::Mock.new
|
125
|
+
matcher.expect :evaluate, nil
|
126
|
+
base.stub(:matcher, matcher) do
|
127
|
+
base.evaluate
|
128
|
+
end
|
129
|
+
matcher.verify
|
130
|
+
end
|
131
|
+
end
|
132
|
+
|
133
|
+
describe 'interfaces to matcher classes' do
|
134
|
+
|
135
|
+
describe '#matcher_for' do
|
136
|
+
it "creates a matcher for the given class name" do
|
137
|
+
matcher = base.matcher_for(:equivalence)
|
138
|
+
matcher.must_be_instance_of Casino::Intersection::Match::Equivalence
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe '#match_arguments' do
|
143
|
+
let(:arguments) { [base.key, base.value, base.document, base.field] }
|
144
|
+
subject { base.match_arguments }
|
145
|
+
it { subject.must_equal arguments }
|
146
|
+
end
|
147
|
+
|
148
|
+
describe '#matcher' do
|
149
|
+
it "finds the first eligible matcher" do
|
150
|
+
eligible_matcher = OpenStruct.new(eligible?: true)
|
151
|
+
base.stub(:matchers, [eligible_matcher]) do
|
152
|
+
base.matcher.must_equal eligible_matcher
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
end
|
158
|
+
|
159
|
+
end
|