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
data/.gitignore
ADDED
data/.ruby-gemset
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
acumen
|
data/.ruby-version
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
ruby-1.9.3-p385
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2013 Joseph McCormick
|
2
|
+
|
3
|
+
MIT License
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
6
|
+
a copy of this software and associated documentation files (the
|
7
|
+
"Software"), to deal in the Software without restriction, including
|
8
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
9
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
10
|
+
permit persons to whom the Software is furnished to do so, subject to
|
11
|
+
the following conditions:
|
12
|
+
|
13
|
+
The above copyright notice and this permission notice shall be
|
14
|
+
included in all copies or substantial portions of the Software.
|
15
|
+
|
16
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
17
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
18
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
19
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
20
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
21
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
22
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,93 @@
|
|
1
|
+
# Casino
|
2
|
+
|
3
|
+
Casino is a DSL which helps you interact with and explore your `Mongoid::Document` collections, and helps you more easily use MongoDB's aggregation framework.
|
4
|
+
|
5
|
+
### Casino works like this:
|
6
|
+
|
7
|
+
1. Build a class and include Casino::Collection
|
8
|
+
* This gives the class the following macros: `dimension`, `focus`, and `question`
|
9
|
+
* The class also gains the following instance methods: `answer`, `intersection`, `projection`, and `query`
|
10
|
+
|
11
|
+
2. Declare the parameters of your Casino class by defining it using the class-level macros. You must define at least one `focus`, `dimension`, and `question`
|
12
|
+
* A `focus` is a class name or a list of class names where Casino can expect to find a Mongoid document API (in other words, a class which includes `Mongoid::Document`)
|
13
|
+
* A `dimension` defines an area of interest for the focus by name, then points to a specific `Mongoid::Document` field with a symbol or string. This is usually something defined in the document model itself, but it can actually be anything, due to the schemaless nature of MongoDB.
|
14
|
+
The third argument to a dimension is a symbol which points to an `attr_accessor` or instance method on the class which provides an array of `query` method invocations. This list represents the different facets of the dimension.
|
15
|
+
* Finally, a `question` is a specific action you want to use once you've produced the documents that your `dimension` and `focus` combinations have resulted in.
|
16
|
+
In order to tap into the current intersection being queried, Casino provides the `intersection` helper, which is a combination of all the current dimension facets currently being questioned.
|
17
|
+
|
18
|
+
3. Once defined, Casino will handle generation and querying of your generated collection from here on out.
|
19
|
+
|
20
|
+
## The Casino API
|
21
|
+
|
22
|
+
### Class (macro) methods
|
23
|
+
|
24
|
+
#### Dimension
|
25
|
+
* **Arguments:**
|
26
|
+
* *Label:* A "pretty name" for the dimension for easy display.
|
27
|
+
* *Field:* This is a string or symbol which indicates the dimension's target field.
|
28
|
+
* *Queries:* This is a symbol which references an instance method or accessor that produces an array of `Casino::Query` objects (built with the `query` instance method).
|
29
|
+
* *Approach: (optional)* This is a hash which contains an `:operator` key, in case the dimension should query a field differently than the default (`where`) query plan, for example, `gt`, or `and`, etc.
|
30
|
+
* **Returns:** A `Casino::Dimension` object.
|
31
|
+
* **What it does:** The `dimension` class method creates a `Casino::Dimension` object and registers it with the `Casino::Collection` the parent class is a part of. From that point on, the dimension will be used as a factor in building out the following:
|
32
|
+
* A generated collection key (`_id`).
|
33
|
+
* The generated collection name.
|
34
|
+
* The intersections for the aggregation, where Casino will ask any questions it is given.
|
35
|
+
|
36
|
+
#### Focus
|
37
|
+
* **Arguments:**
|
38
|
+
* *Models:* A class which includes the `Mongoid::Document` module.
|
39
|
+
* **Returns:** A `Casino::Focus` object.
|
40
|
+
* **What it does:** Creates a `Casino::Focus` object and registers it with the `Casino::Collection` of the parent class. From that point on, the focus object is used in the following way:
|
41
|
+
* The generated collection name.
|
42
|
+
* The targets for dimensional intersection questions.
|
43
|
+
* The subject material for aggregate data.
|
44
|
+
|
45
|
+
#### Question
|
46
|
+
* **Arguments:**
|
47
|
+
* *Name:* The name of the question, this is what the question will be registered under.
|
48
|
+
* *Answer:* This is a `:method_name`.
|
49
|
+
* **Returns:** A `Casino::Question` object
|
50
|
+
* **What it does:** Creates a `Casino::Question` object for the given arguments, and registers it with the `Casino::Collection` of the parent class.
|
51
|
+
|
52
|
+
A `Casino::Question` represents the actual operation which will be performed on a focus at a given dimensional intersection.
|
53
|
+
|
54
|
+
When methods marked with the `question` macro are run, the following instance variables become available:
|
55
|
+
|
56
|
+
* `answer`: An answer to another question at the current intersection.
|
57
|
+
* `intersection`: The current intersection criteria.
|
58
|
+
* `projection`: A MongoDB aggregation framework wrapper scoped to the current intersection.
|
59
|
+
|
60
|
+
### Instance methods
|
61
|
+
|
62
|
+
#### Answer
|
63
|
+
* **Arguments:**
|
64
|
+
* *Method name:* A `:symbol` pointing to another question. This method returns the results of that question.
|
65
|
+
* **Returns:** An answered question key/value pair: `{ question_name: question_answer }`
|
66
|
+
|
67
|
+
#### Insert
|
68
|
+
* **Arguments:**
|
69
|
+
* *Documents:* Any argument passed in will be evaluated as though it were a `Mongoid::Document` of the collection's focus.
|
70
|
+
* **Returns:** Any results which were updated with the given documents.
|
71
|
+
* **What it does:** `insert` is a trickle-in mechanism, which updates any intersections which match a document.
|
72
|
+
|
73
|
+
#### Intersection
|
74
|
+
* **Arguments:** None
|
75
|
+
* **Returns:** `Mongoid::Criteria` - A window into the currently active criteria being questioned.
|
76
|
+
|
77
|
+
#### Projection
|
78
|
+
* **Arguments:** None
|
79
|
+
* **Returns:** A `Casino::Projection` object preloaded with the current intersection selector, which wraps around MongoDB's aggregation framework.
|
80
|
+
|
81
|
+
#### Query
|
82
|
+
* **Arguments:**
|
83
|
+
* *Label:* A string or symbol that indicates a field to store the query's results in
|
84
|
+
* *Criterion:* A list of items to use to search within the `question` field
|
85
|
+
* **Returns:** A `Casino::Query` instance for use to build outputs as well as to build `Casino::Intersection` objects with.
|
86
|
+
|
87
|
+
#### Results
|
88
|
+
* **Arguments:** None
|
89
|
+
* **Returns:** A list of answer hashes for every intersection. Casino persists intersection results to a generated MongoDB collection the first time it is used, and afterward will just combine already produced results with new results (in essence, it only queries for new information and remembers the rest).
|
90
|
+
|
91
|
+
#### Update
|
92
|
+
* **Arguments:** None
|
93
|
+
* **Returns:** Re-generates the entire generated collection.
|
data/Rakefile
ADDED
data/casino.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'casino/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
|
8
|
+
gem.name = "mongoid-casino"
|
9
|
+
gem.version = Casino::VERSION
|
10
|
+
gem.authors = ["JC McCormick"]
|
11
|
+
gem.email = ["esmevane@gmail.com"]
|
12
|
+
gem.description = %q{Create and maintain aggregate metadata with Mongoid}
|
13
|
+
gem.summary = %q{Aggregation handler for Mongoid}
|
14
|
+
gem.homepage = "http://www.github.com/esmevane/casino"
|
15
|
+
|
16
|
+
gem.files = `git ls-files`.split($/)
|
17
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
18
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
19
|
+
gem.require_paths = ["lib"]
|
20
|
+
|
21
|
+
gem.add_dependency "mongoid", "3.0.19"
|
22
|
+
|
23
|
+
gem.add_development_dependency "database_cleaner", "~> 0.9"
|
24
|
+
gem.add_development_dependency "fabrication", "~> 2.6"
|
25
|
+
gem.add_development_dependency "json", "~> 1.7"
|
26
|
+
gem.add_development_dependency "minitest", "~> 4.3"
|
27
|
+
gem.add_development_dependency "rake", "~> 10.0"
|
28
|
+
gem.add_development_dependency "simplecov", "~> 0.7"
|
29
|
+
|
30
|
+
end
|
data/lib/casino.rb
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'csv'
|
2
|
+
require 'mongoid'
|
3
|
+
|
4
|
+
libs = %w(collection dimension focus intersection lobby projection
|
5
|
+
query question store version)
|
6
|
+
path = File.dirname(__FILE__)
|
7
|
+
|
8
|
+
libs.each do |lib|
|
9
|
+
require "#{path}/casino/#{lib}"
|
10
|
+
end
|
11
|
+
|
12
|
+
module Casino
|
13
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
module Casino
|
2
|
+
module Collection
|
3
|
+
attr_reader :intersection, :answer
|
4
|
+
|
5
|
+
def self.included(base)
|
6
|
+
base.extend ClassMethods
|
7
|
+
end
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def dimension(label, field, queries, approach = Hash.new)
|
12
|
+
register Dimension.new(label, field, queries, approach)
|
13
|
+
end
|
14
|
+
|
15
|
+
def question(name, answer)
|
16
|
+
register Question.new(name, answer)
|
17
|
+
end
|
18
|
+
|
19
|
+
def focus(model)
|
20
|
+
register Focus.new(model)
|
21
|
+
end
|
22
|
+
|
23
|
+
def lobby
|
24
|
+
@lobby ||= Lobby.new(self)
|
25
|
+
end
|
26
|
+
|
27
|
+
def register(object)
|
28
|
+
lobby.add_registry(object)
|
29
|
+
object
|
30
|
+
end
|
31
|
+
|
32
|
+
end
|
33
|
+
|
34
|
+
def dimensions
|
35
|
+
registry[:dimension]
|
36
|
+
end
|
37
|
+
|
38
|
+
def questions
|
39
|
+
registry[:question]
|
40
|
+
end
|
41
|
+
|
42
|
+
def focus
|
43
|
+
registry[:focus]
|
44
|
+
end
|
45
|
+
|
46
|
+
def store
|
47
|
+
@store ||= Store.new key
|
48
|
+
end
|
49
|
+
|
50
|
+
def key
|
51
|
+
Digest::SHA1.hexdigest(key_base)
|
52
|
+
end
|
53
|
+
|
54
|
+
def projection
|
55
|
+
Casino::Projection.new(focus_model).where(intersection.selector)
|
56
|
+
end
|
57
|
+
|
58
|
+
def answer(method_name)
|
59
|
+
send(method_name)
|
60
|
+
end
|
61
|
+
|
62
|
+
def query(label, *criteria)
|
63
|
+
Query.new(label, [], *criteria)
|
64
|
+
end
|
65
|
+
|
66
|
+
def results
|
67
|
+
persist_results if pending_results.any?
|
68
|
+
stored_results
|
69
|
+
end
|
70
|
+
|
71
|
+
def update
|
72
|
+
merge all_results
|
73
|
+
stored_results
|
74
|
+
end
|
75
|
+
|
76
|
+
def intersection
|
77
|
+
@intersection || focus_model.scoped
|
78
|
+
end
|
79
|
+
|
80
|
+
def insert(*documents)
|
81
|
+
matching_intersections = match_intersections documents
|
82
|
+
matching_results = determine_results matching_intersections
|
83
|
+
merge matching_results
|
84
|
+
stored_results.where(id: matching_results.map(&:id))
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def queries(dimension)
|
90
|
+
send(dimension.queries).map { |query| query.merge(dimension) }
|
91
|
+
end
|
92
|
+
|
93
|
+
def intersections
|
94
|
+
focus.reduce(Array.new) do |list, answer_focus|
|
95
|
+
list + intersections_for(answer_focus)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
|
99
|
+
def merge(documents)
|
100
|
+
store.merge *documents
|
101
|
+
end
|
102
|
+
|
103
|
+
def persist_results
|
104
|
+
merge collected_results
|
105
|
+
end
|
106
|
+
|
107
|
+
def collected_results
|
108
|
+
stored_results + pending_results
|
109
|
+
end
|
110
|
+
|
111
|
+
def pending_results
|
112
|
+
determine_results pending_intersections
|
113
|
+
end
|
114
|
+
|
115
|
+
def all_results
|
116
|
+
determine_results intersections
|
117
|
+
end
|
118
|
+
|
119
|
+
def answers
|
120
|
+
questions.reduce(Hash.new) do |hash, question|
|
121
|
+
hash.merge question.name => send(question.answer)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
125
|
+
def stored_results
|
126
|
+
store.in(id: intersection_ids)
|
127
|
+
end
|
128
|
+
|
129
|
+
def intersection_ids
|
130
|
+
intersections.map(&:selector)
|
131
|
+
end
|
132
|
+
|
133
|
+
def determine_results(given_intersections)
|
134
|
+
given_intersections.map do |current_intersection|
|
135
|
+
@intersection = current_intersection.criteria
|
136
|
+
result = { _id: intersection.selector }.merge(answers)
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def key_base
|
141
|
+
join = "_and_"
|
142
|
+
what = questions.map(&:name).join(join)
|
143
|
+
parameters = dimensions.map(&:field).join(join)
|
144
|
+
my_name = self.class.name.underscore
|
145
|
+
targets = focus.map(&:model).map(&:name).map(&:underscore).sort.
|
146
|
+
join(join)
|
147
|
+
"#{my_name}_asks_#{targets}_about_#{what}_by_#{parameters}"
|
148
|
+
end
|
149
|
+
|
150
|
+
def match_intersections(documents)
|
151
|
+
documents.reduce(Array.new) do |list, document|
|
152
|
+
list + matches_for(document)
|
153
|
+
end.uniq
|
154
|
+
end
|
155
|
+
|
156
|
+
def matches_for(document)
|
157
|
+
intersections.select { |intersection| intersection.match?(document) }
|
158
|
+
end
|
159
|
+
|
160
|
+
def pending_intersections
|
161
|
+
ids = stored_results.map(&:id)
|
162
|
+
intersections.reject do |intersection|
|
163
|
+
ids.include? intersection.selector
|
164
|
+
end
|
165
|
+
end
|
166
|
+
|
167
|
+
def focus_model
|
168
|
+
focus.map(&:model).first
|
169
|
+
end
|
170
|
+
|
171
|
+
def all_queries
|
172
|
+
dimensions.map { |dimension| queries(dimension) }
|
173
|
+
end
|
174
|
+
|
175
|
+
def intersections_for(answer_focus)
|
176
|
+
product_of_all_queries.map do |conditions|
|
177
|
+
label = conditions.map(&:label)
|
178
|
+
Intersection.new label, answer_focus.build_criteria(*conditions)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
def get_product(head, *tail)
|
183
|
+
head.product *tail
|
184
|
+
end
|
185
|
+
|
186
|
+
def product_of_all_queries
|
187
|
+
get_product(*all_queries)
|
188
|
+
end
|
189
|
+
|
190
|
+
def registry
|
191
|
+
self.class.lobby.registry
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Casino
|
2
|
+
class Dimension
|
3
|
+
attr_accessor :label, :field, :queries, :approach
|
4
|
+
def initialize(label, field, queries, approach = Hash.new)
|
5
|
+
self.label = label
|
6
|
+
self.field = field
|
7
|
+
self.queries = queries
|
8
|
+
self.approach = { operator: :where }.merge(approach)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
data/lib/casino/focus.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Casino
|
2
|
+
class Focus
|
3
|
+
attr_accessor :model
|
4
|
+
|
5
|
+
def initialize(model)
|
6
|
+
self.model = model
|
7
|
+
end
|
8
|
+
|
9
|
+
def hash
|
10
|
+
model.hash
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
other.is_a?(self.class) && other.model == model
|
15
|
+
end
|
16
|
+
alias :eql? :==
|
17
|
+
|
18
|
+
def build_criteria(*queries)
|
19
|
+
conditions = queries.map(&:conditions).flatten(1)
|
20
|
+
conditions.reduce(model) do |criteria, condition_pair|
|
21
|
+
criteria.send(condition_pair.first, condition_pair.last)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
libs = %w(equivalence base all include greater lesser recurse expression)
|
2
|
+
path = File.dirname(__FILE__)
|
3
|
+
|
4
|
+
libs.each do |lib|
|
5
|
+
require "#{path}/intersection/match/#{lib}"
|
6
|
+
end
|
7
|
+
|
8
|
+
module Casino
|
9
|
+
class Intersection
|
10
|
+
attr_accessor :label, :criteria
|
11
|
+
|
12
|
+
delegate :selector, to: :criteria
|
13
|
+
|
14
|
+
def initialize(label, criteria)
|
15
|
+
self.label = label
|
16
|
+
self.criteria = criteria
|
17
|
+
end
|
18
|
+
|
19
|
+
def match?(document)
|
20
|
+
selector.keys.map { |key| match_key_against(document, key) }.all?
|
21
|
+
end
|
22
|
+
|
23
|
+
private
|
24
|
+
|
25
|
+
def match_key_against(document, key)
|
26
|
+
Match::Base.new(document, key, selector).evaluate
|
27
|
+
end
|
28
|
+
|
29
|
+
end
|
30
|
+
end
|