mongo_aggregation_dsl 0.0.0.pre4
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.circleci/config.yml +61 -0
- data/.gitignore +54 -0
- data/.reek.yml +5 -0
- data/.rspec +2 -0
- data/.rubocop.yml +51 -0
- data/.ruby-version +1 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +205 -0
- data/README.md +56 -0
- data/Rakefile +8 -0
- data/codecov.yml +3 -0
- data/config/pronto-circleci.yml +7 -0
- data/lib/aggregate/contracts/class_includes.rb +26 -0
- data/lib/aggregate/contracts/hash_max_length.rb +24 -0
- data/lib/aggregate/contracts/hash_min_length.rb +24 -0
- data/lib/aggregate/contracts/hash_value_type.rb +31 -0
- data/lib/aggregate/contracts/starts_with.rb +24 -0
- data/lib/aggregate/contracts.rb +8 -0
- data/lib/aggregate/pipeline.rb +41 -0
- data/lib/aggregate/stages/base.rb +20 -0
- data/lib/aggregate/stages/facet.rb +24 -0
- data/lib/aggregate/stages/group.rb +25 -0
- data/lib/aggregate/stages/hash_base.rb +13 -0
- data/lib/aggregate/stages/lookup.rb +26 -0
- data/lib/aggregate/stages/match.rb +25 -0
- data/lib/aggregate/stages/project.rb +17 -0
- data/lib/aggregate/stages/replace_root.rb +22 -0
- data/lib/aggregate/stages/unwind.rb +29 -0
- data/lib/aggregate/stages.rb +10 -0
- data/lib/aggregate/values/array.rb +20 -0
- data/lib/aggregate/values/base.rb +46 -0
- data/lib/aggregate/values/document_class.rb +27 -0
- data/lib/aggregate/values/hash.rb +43 -0
- data/lib/aggregate/values/nil.rb +25 -0
- data/lib/aggregate/values/object_id.rb +24 -0
- data/lib/aggregate/values/string.rb +22 -0
- data/lib/aggregate/values/symbol.rb +52 -0
- data/lib/aggregate/values.rb +8 -0
- data/lib/aggregate.rb +19 -0
- data/lib/mongo_aggregation_dsl.rb +3 -0
- data/mongo_aggregation_dsl.gemspec +42 -0
- 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,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
|
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,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
|