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