mongo_aggregation_dsl 0.0.0.pre4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +61 -0
  3. data/.gitignore +54 -0
  4. data/.reek.yml +5 -0
  5. data/.rspec +2 -0
  6. data/.rubocop.yml +51 -0
  7. data/.ruby-version +1 -0
  8. data/Gemfile +10 -0
  9. data/Gemfile.lock +205 -0
  10. data/README.md +56 -0
  11. data/Rakefile +8 -0
  12. data/codecov.yml +3 -0
  13. data/config/pronto-circleci.yml +7 -0
  14. data/lib/aggregate/contracts/class_includes.rb +26 -0
  15. data/lib/aggregate/contracts/hash_max_length.rb +24 -0
  16. data/lib/aggregate/contracts/hash_min_length.rb +24 -0
  17. data/lib/aggregate/contracts/hash_value_type.rb +31 -0
  18. data/lib/aggregate/contracts/starts_with.rb +24 -0
  19. data/lib/aggregate/contracts.rb +8 -0
  20. data/lib/aggregate/pipeline.rb +41 -0
  21. data/lib/aggregate/stages/base.rb +20 -0
  22. data/lib/aggregate/stages/facet.rb +24 -0
  23. data/lib/aggregate/stages/group.rb +25 -0
  24. data/lib/aggregate/stages/hash_base.rb +13 -0
  25. data/lib/aggregate/stages/lookup.rb +26 -0
  26. data/lib/aggregate/stages/match.rb +25 -0
  27. data/lib/aggregate/stages/project.rb +17 -0
  28. data/lib/aggregate/stages/replace_root.rb +22 -0
  29. data/lib/aggregate/stages/unwind.rb +29 -0
  30. data/lib/aggregate/stages.rb +10 -0
  31. data/lib/aggregate/values/array.rb +20 -0
  32. data/lib/aggregate/values/base.rb +46 -0
  33. data/lib/aggregate/values/document_class.rb +27 -0
  34. data/lib/aggregate/values/hash.rb +43 -0
  35. data/lib/aggregate/values/nil.rb +25 -0
  36. data/lib/aggregate/values/object_id.rb +24 -0
  37. data/lib/aggregate/values/string.rb +22 -0
  38. data/lib/aggregate/values/symbol.rb +52 -0
  39. data/lib/aggregate/values.rb +8 -0
  40. data/lib/aggregate.rb +19 -0
  41. data/lib/mongo_aggregation_dsl.rb +3 -0
  42. data/mongo_aggregation_dsl.gemspec +42 -0
  43. metadata +385 -0
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Represents an aggregation facet
6
+ # https://docs.mongodb.com/manual/reference/operator/aggregation/facet/#pipe._S_facet
7
+ class Facet < HashBase
8
+ Contract And[
9
+ C::HashMinLength[1],
10
+ HashOf[Symbol, Aggregate::Pipeline]] => Any
11
+ def initialize(options)
12
+ super(options)
13
+ end
14
+
15
+ def to_s
16
+ inspect
17
+ end
18
+
19
+ def inspect
20
+ "{ $facet: #{options} }"
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Represents an aggregation group
6
+ # https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pipe._S_group
7
+ class Group < HashBase
8
+ Contract And[
9
+ Not[C::HashValueType[0, Hash]],
10
+ C::HashValueType[1..-1, Hash]
11
+ ] => Any
12
+ def initialize(options)
13
+ super(options)
14
+ end
15
+
16
+ def to_s
17
+ inspect
18
+ end
19
+
20
+ def inspect
21
+ "{ $group: #{options} }"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Base class for all stages that take a hash as an initializer option
6
+ class HashBase < Base
7
+ Contract C::HashMinLength[1] => Any
8
+ def initialize(options)
9
+ super(Aggregate::Values::Hash.new(options, false))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Represents an aggregation lookup
6
+ # https://docs.mongodb.com/manual/reference/operator/aggregation/lookup/#pipe._S_lookup
7
+ class Lookup < HashBase
8
+ Contract KeywordArgs[
9
+ from: C::ClassIncludes[[Mongoid::Document]],
10
+ as: String,
11
+ let: And[HashOf[Symbol, Symbol], C::HashMinLength[1]],
12
+ pipeline: Aggregate::Pipeline] => Any
13
+ def initialize(options)
14
+ super(options)
15
+ end
16
+
17
+ def to_s
18
+ inspect
19
+ end
20
+
21
+ def inspect
22
+ "{ $lookup: #{options} }"
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Represents an aggregation match
6
+ # https://docs.mongodb.com/manual/reference/operator/aggregation/match/#pipe._S_match
7
+ # TODO: Should probably have two match types.
8
+ # A simple match `match(account_id: BSON::ObjectId("11111111111111111111"))`
9
+ # An expression match `match( :expr.and => [ { eq: %w[$user_id $$user_id] }] )`
10
+ class Match < HashBase
11
+ Contract Or[HashOf[Symbol, Any], HashOf[Origin::Key, Any]] => Any
12
+ def initialize(options)
13
+ super(options)
14
+ end
15
+
16
+ def to_s
17
+ inspect
18
+ end
19
+
20
+ def inspect
21
+ "{ $match: #{options} }"
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Represents an aggregation project
6
+ # https://docs.mongodb.com/manual/reference/operator/aggregation/project/#pipe._S_project
7
+ class Project < HashBase
8
+ def to_s
9
+ inspect
10
+ end
11
+
12
+ def inspect
13
+ "{ $project: #{options} }"
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Represents an aggregation replaceRoot
6
+ # https://docs.mongodb.com/manual/reference/operator/aggregation/replaceRoot/#pipe._S_replaceRoot
7
+ class ReplaceRoot < Base
8
+ Contract And[String, C::StartsWith["$"]] => Any
9
+ def initialize(new_root)
10
+ super(new_root)
11
+ end
12
+
13
+ def to_s
14
+ inspect
15
+ end
16
+
17
+ def inspect
18
+ "{ $replaceRoot: {newRoot: #{options}} }"
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Stages
5
+ # Represents an aggregation unwind
6
+ # https://docs.mongodb.com/manual/reference/operator/aggregation/unwind/#pipe._S_unwind
7
+ class Unwind < Base
8
+ Contract Or[
9
+ And[String, C::StartsWith["$"]],
10
+ KeywordArgs[
11
+ path: And[String, C::StartsWith["$"]],
12
+ includeArrayIndex: Optional[Boolean],
13
+ preserveNullAndEmptyArrays: Optional[Boolean]
14
+ ],
15
+ ] => Any
16
+ def initialize(options)
17
+ super(options)
18
+ end
19
+
20
+ def to_s
21
+ inspect
22
+ end
23
+
24
+ def inspect
25
+ "{ $unwind: #{options} }"
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ # Stages represent mongo aggregate pipeline stages.
5
+ # All classes which in this namespace have methods
6
+ # dynamically added to the Aggregate::Pipeline
7
+ module Stages
8
+ Autoloaded.module {}
9
+ end
10
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # Takes an array and converts each to the appropriate value handler if one is available.
6
+ class Array < Base
7
+ def to_s
8
+ raise ArgumentError, "Array cannot be a hash key" if is_key
9
+
10
+ "[#{value.map { |value| get_value(value, false) }.join(', ')}]"
11
+ end
12
+
13
+ class << self
14
+ def handles?(value)
15
+ value.is_a? ::Array
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # derived value handlers are required to implement the following contract
6
+ # def to_s
7
+ # # Returns a properly formatted string for the given value. Note that care
8
+ # # should be taken to properly format as a hash key or value depending on is_key
9
+ # end
10
+ # class << self
11
+ # def handles?(value)
12
+ # # returns true if this value handler can handle transforming the given value type into a string
13
+ # end
14
+ # end
15
+ #
16
+ # In keeping with duck typing the above are not implemented here and will not raise
17
+ # a NotImplementedError. Instead a standard MethodMissing will be raised if a value handler
18
+ # cannot be called with `handles?(value)` or `to_s`
19
+ #
20
+ # http://chrisstump.online/2016/03/23/stop-abusing-notimplementederror/
21
+ class Base
22
+ attr_reader :value, :is_key
23
+ def initialize(value, is_key)
24
+ @value = value
25
+ @is_key = is_key
26
+ end
27
+
28
+ class << self
29
+ def value_handlers
30
+ @value_handlers ||= (Aggregate::Values.constants - [:Base]).map do |klass|
31
+ "Aggregate::Values::#{klass}".constantize
32
+ end
33
+ end
34
+ end
35
+
36
+ private
37
+
38
+ private
39
+
40
+ def get_value(original_value, is_hash_key)
41
+ handler = self.class.value_handlers.detect { |handler| handler.handles?(original_value) }
42
+ handler.nil? ? original_value : handler.new(original_value, is_hash_key)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # Takes a mongoid document and returns a string containing the document collection name
6
+ class DocumentClass < Base
7
+ def to_s
8
+ inspect
9
+ end
10
+
11
+ def inspect
12
+ raise ArgumentError("Document cannot be a hash key") if is_key
13
+
14
+ "'#{value.collection_name}'"
15
+ end
16
+
17
+ class << self
18
+ # :reek:ManualDispatch
19
+ def handles?(value)
20
+ return false unless value.respond_to? :included_modules
21
+
22
+ value.included_modules.include?(Mongoid::Document)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # Takes hash and converts each key and pair to the appropriate value handler if one is available.
6
+ class Hash < Base
7
+ def to_s
8
+ inspect
9
+ end
10
+
11
+ def inspect
12
+ raise ArgumentError("Hash cannot be a hash key") if is_key
13
+
14
+ "{ #{transpose_options.map { |hash_key, hash_value| "#{hash_key}: #{hash_value}" }.join(', ')} }"
15
+ end
16
+
17
+ private
18
+
19
+ def transpose_options
20
+ new_hash = {}
21
+ value.each do |original_key, original_value|
22
+ if original_key.is_a?(Origin::Key)
23
+ expression = original_key.__expr_part__(original_value)
24
+ original_key = expression.keys.first.to_sym
25
+ original_value = expression.values.first
26
+ end
27
+
28
+ new_key = get_value(original_key, true)
29
+ new_value = get_value(original_value, false)
30
+
31
+ new_hash[new_key] = new_value
32
+ end
33
+ new_hash
34
+ end
35
+
36
+ class << self
37
+ def handles?(value)
38
+ value.is_a? ::Hash
39
+ end
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # Converts nil to null
6
+ # :reek:NilCheck
7
+ class Nil < Base
8
+ def to_s
9
+ inspect
10
+ end
11
+
12
+ def inspect
13
+ raise ArgumentError, "Nil cannot be a hash key" if is_key
14
+
15
+ "null"
16
+ end
17
+
18
+ class << self
19
+ def handles?(value)
20
+ value.nil?
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # Converts a BSON::Object id to a string `ObjectId('123')`
6
+ class ObjectId < Base
7
+ def to_s
8
+ inspect
9
+ end
10
+
11
+ def inspect
12
+ raise ArgumentError, "ObjectId cannot be a hash key" if is_key
13
+
14
+ "ObjectId('#{value}')"
15
+ end
16
+
17
+ class << self
18
+ def handles?(value)
19
+ value.is_a? BSON::ObjectId
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # Returns a string wrapped in single quotes if the value or string if a key
6
+ class String < Base
7
+ def to_s
8
+ inspect
9
+ end
10
+
11
+ def inspect
12
+ is_key ? value.to_s : "'#{value}'"
13
+ end
14
+
15
+ class << self
16
+ def handles?(value)
17
+ value.is_a? ::String
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ module Values
5
+ # Returns a string wrapped in single quotes if the value or string if a key
6
+ # Additionally adds $ to the reserved aggregation operations.
7
+ class Symbol < Base
8
+ # rubocop:disable Layout/AlignArray
9
+ EXPRESSION_OPERATORS = %i[
10
+ abs add ceil divide exp floor ln log log10 mod pow sqrt subtract trunc
11
+ arrayElemAt arrayToObject concatArrays filter in indexOfArray isArray map objectToArray
12
+ range reduce reverseArray size slice zip
13
+ and not or
14
+ cmp eq gt gte lt lte ne
15
+ cond ifNull switch
16
+ dateFromParts dateFromString dateToParts dateToString dayOfMonth dayOfWeek dayOfYear
17
+ hour isoDayOfWeek isoWeek isoWeekYear millisecond minute month second toDate week year
18
+ literal
19
+ mergeObjects
20
+ objectToArray
21
+ allElementsTrue anyElementTrue setDifference setEquals setIntersection setIsSubset setUnion
22
+ concat dateFromString dateToString indexOfBytes indexOfCP ltrim rtrim split strLenCP
23
+ strcasecmp substr substrBytes substrCP toLower toString trim toUpper
24
+ meta
25
+ convert toBool toDate toDecimal toDouble toInt toLong toObjectId toString type
26
+ ].freeze
27
+ # rubocop:enable Layout/AlignArray
28
+
29
+ GROUP_ACCUMULATORS = %i[avg first last max min push addToSet].freeze
30
+
31
+ PROJECT_ACCUMULATORS = %i[avg max min push stdDevPop stdDevSamp sum].freeze
32
+ def to_s
33
+ inspect
34
+ end
35
+
36
+ def inspect
37
+ is_operator = (%i[expr] + EXPRESSION_OPERATORS + GROUP_ACCUMULATORS + PROJECT_ACCUMULATORS).include?(value)
38
+ retval = is_operator ? "$#{value}" : value
39
+
40
+ retval = :_id if value == :id
41
+
42
+ is_key ? retval.to_s : "'#{retval}'"
43
+ end
44
+
45
+ class << self
46
+ def handles?(value)
47
+ value.is_a? ::Symbol
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Aggregate
4
+ # Values handle transformation of known types into a correct string format
5
+ module Values
6
+ Autoloaded.module {}
7
+ end
8
+ end
data/lib/aggregate.rb ADDED
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "autoloaded"
4
+ require "contracts"
5
+ require "mongo"
6
+ require "mongoid"
7
+ require "origin"
8
+
9
+ # Extend Symbol with origin handler functions
10
+ Symbol.add_key(:and, "$and", "$and")
11
+ Symbol.add_key(:nor, "$nor", "$nor")
12
+ Symbol.add_key(:or, "$or", "$or")
13
+
14
+ # The base module for the gem under which all classes are namespaced.
15
+ module Aggregate
16
+ Autoloaded.module {}
17
+ end
18
+
19
+ C = Aggregate::Contracts
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "aggregate"
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "mongo_aggregation_dsl"
5
+ spec.version = "0.0.0.pre4"
6
+ spec.authors = ["KrimsonKla"]
7
+ spec.email = ["admin@cardtapp.com"]
8
+ spec.date = "2018-09-10"
9
+ spec.summary = "An aggregation DSL for use with mongo ruby driver"
10
+
11
+ spec.homepage = "http://www.cardtapp.com"
12
+ spec.license = "MIT"
13
+
14
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
15
+ f.match(%r{^(test|spec|features)/})
16
+ end
17
+
18
+ spec.require_paths = ["lib"]
19
+
20
+ spec.add_dependency "autoloaded", "~> 2"
21
+ spec.add_dependency "contracts-lite"
22
+ spec.add_dependency "mongo", "~> 2.6.2"
23
+ spec.add_dependency "mongoid", "~> 5.2.0"
24
+ spec.add_dependency "origin", "~> 2.3.1"
25
+
26
+ spec.add_development_dependency "bundler", "~> 1.16"
27
+ spec.add_development_dependency "codecov", "~> 0.1", "~> 0.1.0"
28
+ spec.add_development_dependency "database_cleaner"
29
+ spec.add_development_dependency "pronto"
30
+ spec.add_development_dependency "pronto-brakeman"
31
+ spec.add_development_dependency "pronto-circleci"
32
+ spec.add_development_dependency "pronto-fasterer"
33
+ spec.add_development_dependency "pronto-rails_best_practices"
34
+ spec.add_development_dependency "pronto-reek"
35
+ spec.add_development_dependency "pronto-rubocop"
36
+ spec.add_development_dependency "rake", "~> 10.0"
37
+ spec.add_development_dependency "rspec", "~> 3.0"
38
+ spec.add_development_dependency "rspec_junit_formatter", "~> 0.3.0"
39
+ spec.add_development_dependency "rubocop"
40
+ spec.add_development_dependency "simplecov"
41
+ spec.add_development_dependency "simplecov-rcov"
42
+ end