mongoid-casino 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. data/.gitignore +20 -0
  2. data/.ruby-gemset +1 -0
  3. data/.ruby-version +1 -0
  4. data/Gemfile +4 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +93 -0
  7. data/Rakefile +9 -0
  8. data/casino.gemspec +30 -0
  9. data/lib/casino.rb +13 -0
  10. data/lib/casino/collection.rb +195 -0
  11. data/lib/casino/dimension.rb +11 -0
  12. data/lib/casino/focus.rb +26 -0
  13. data/lib/casino/intersection.rb +30 -0
  14. data/lib/casino/intersection/match/all.rb +29 -0
  15. data/lib/casino/intersection/match/base.rb +49 -0
  16. data/lib/casino/intersection/match/equivalence.rb +25 -0
  17. data/lib/casino/intersection/match/expression.rb +15 -0
  18. data/lib/casino/intersection/match/greater.rb +43 -0
  19. data/lib/casino/intersection/match/include.rb +15 -0
  20. data/lib/casino/intersection/match/lesser.rb +31 -0
  21. data/lib/casino/intersection/match/recurse.rb +19 -0
  22. data/lib/casino/lobby.rb +21 -0
  23. data/lib/casino/projection.rb +76 -0
  24. data/lib/casino/query.rb +23 -0
  25. data/lib/casino/question.rb +9 -0
  26. data/lib/casino/store.rb +49 -0
  27. data/lib/casino/version.rb +3 -0
  28. data/spec/fabricators/model_fabricator.rb +12 -0
  29. data/spec/fabricators/note_fabricator.rb +3 -0
  30. data/spec/fixtures/collections/collection.rb +3 -0
  31. data/spec/fixtures/collections/emails_by_day.rb +28 -0
  32. data/spec/fixtures/models/model.rb +8 -0
  33. data/spec/fixtures/models/note.rb +7 -0
  34. data/spec/lib/casino/collection_spec.rb +146 -0
  35. data/spec/lib/casino/dimension_spec.rb +27 -0
  36. data/spec/lib/casino/focus_spec.rb +53 -0
  37. data/spec/lib/casino/intersection/match/all_spec.rb +24 -0
  38. data/spec/lib/casino/intersection/match/base_spec.rb +159 -0
  39. data/spec/lib/casino/intersection/match/equivalence_spec.rb +26 -0
  40. data/spec/lib/casino/intersection/match/expression_spec.rb +26 -0
  41. data/spec/lib/casino/intersection/match/greater_spec.rb +72 -0
  42. data/spec/lib/casino/intersection/match/include_spec.rb +27 -0
  43. data/spec/lib/casino/intersection/match/lesser_spec.rb +74 -0
  44. data/spec/lib/casino/intersection/match/recurse_spec.rb +24 -0
  45. data/spec/lib/casino/intersection_spec.rb +33 -0
  46. data/spec/lib/casino/lobby_spec.rb +20 -0
  47. data/spec/lib/casino/projection_spec.rb +81 -0
  48. data/spec/lib/casino/query_spec.rb +42 -0
  49. data/spec/lib/casino/question_spec.rb +10 -0
  50. data/spec/lib/casino/store_spec.rb +60 -0
  51. data/spec/spec_helper.rb +26 -0
  52. data/spec/support/mongoid.yml +6 -0
  53. metadata +234 -0
@@ -0,0 +1,20 @@
1
+ *.gem
2
+ *.rbc
3
+ *.un~
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ .rspec
8
+ .rvmrc
9
+ Gemfile.lock
10
+ InstalledFiles
11
+ _yardoc
12
+ coverage
13
+ doc/
14
+ lib/bundler/man
15
+ pkg
16
+ rdoc
17
+ spec/reports
18
+ test/tmp
19
+ test/version_tmp
20
+ tmp
@@ -0,0 +1 @@
1
+ acumen
@@ -0,0 +1 @@
1
+ ruby-1.9.3-p385
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in casino.gemspec
4
+ gemspec
@@ -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.
@@ -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.
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rake/testtask'
3
+
4
+ Rake::TestTask.new do |test_task|
5
+ test_task.libs << "spec"
6
+ test_task.test_files = Dir.glob 'spec/**/*_spec.rb'
7
+ end
8
+
9
+ task default: :test
@@ -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
@@ -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
@@ -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