mongo_aggregation_dsl 0.0.0.pre4

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.
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