mongrep 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 69914b4493a7de6bb8566f431244fc57f17a8a20
4
+ data.tar.gz: 5f24300241239d49e2bacbbcf6d3ce99d8c12327
5
+ SHA512:
6
+ metadata.gz: 96db1d696abc053ea35d709f4cb7694bf555fbdd1d969265a19cac103eefb2f889174c63f8e79158d699abe474a3b94ee3601ad06e1d76a16bc88a1ac5f5ae99
7
+ data.tar.gz: d695e3946bed16a8caa294fa96971a51073226ae44f78a2f5afecd95cb06296f8b423e856ca696e65513f9f9f5c3466b37d39b7d14d7a3e9ffb98d2751642d6b
data/.editorconfig ADDED
@@ -0,0 +1,15 @@
1
+ root = true
2
+
3
+ [*]
4
+ end_of_line = lf
5
+ trim_trailing_whitespace = true
6
+ insert_final_newline = true
7
+ charset = utf-8
8
+ indent_style = space
9
+
10
+ [{Gemfile,*.{rb,gemspec}}]
11
+ indent_size = 2
12
+
13
+ [*.md]
14
+ trim_trailing_whitespace = false
15
+ indent_size = 2
data/.gitignore ADDED
@@ -0,0 +1,22 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/*
6
+ !/coverage/.last_run.json
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /vendor/bundle/
11
+
12
+ # for example coverage
13
+ tmp
14
+
15
+ # exuberant ctags
16
+ tags
17
+
18
+ # yardoc
19
+ .yardoc/
20
+ doc/yardoc
21
+
22
+ log
data/.rspec ADDED
@@ -0,0 +1,2 @@
1
+ --format documentation
2
+ --color
data/.rubocop.yml ADDED
@@ -0,0 +1,6 @@
1
+ require: rubocop-rspec
2
+
3
+ Metrics/BlockLength:
4
+ Exclude:
5
+ - 'spec/**/*.rb'
6
+ - '*.gemspec'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.1
data/.simplecov ADDED
@@ -0,0 +1,9 @@
1
+ unless ARGV.any? { |e| e =~ /guard-rspec/ }
2
+ SimpleCov.start do
3
+ coverage_dir 'log/coverage'
4
+ formatter SimpleCov::Formatter::HTMLFormatter
5
+
6
+ command_name 'rspec'
7
+ maximum_coverage_drop 1
8
+ end
9
+ end
data/.travis.yml ADDED
@@ -0,0 +1,5 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.3.1
5
+ before_install: gem install bundler -v 1.12.5
data/.yardopts ADDED
@@ -0,0 +1,6 @@
1
+ --plugin classmethods
2
+ --charset utf-8
3
+ --markup markdown
4
+ --list-undoc compact
5
+ --output-dir doc/yardoc
6
+ lib/**/*.rb
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ source 'https://rubygems.org'
3
+
4
+ # Specify your gem's dependencies in mongrep.gemspec
5
+ gemspec
data/Guardfile ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ guard :rspec, cmd: 'bundle exec rspec' do
4
+ require 'guard/rspec/dsl'
5
+ dsl = Guard::RSpec::Dsl.new(self)
6
+
7
+ # RSpec files
8
+ rspec = dsl.rspec
9
+ watch(rspec.spec_helper) { rspec.spec_dir }
10
+ watch(rspec.spec_support) { rspec.spec_dir }
11
+ watch(rspec.spec_files)
12
+
13
+ # Ruby files
14
+ ruby = dsl.ruby
15
+ watch(ruby.lib_files) do |m|
16
+ spec_path = m[1][%r{(?<=lib/).*}]
17
+ [
18
+ rspec.spec.call("unit/#{spec_path}"),
19
+ rspec.spec.call("integration/#{spec_path}")
20
+ ]
21
+ end
22
+
23
+ # Shared examples
24
+ watch(%r{(spec/.+)/shared_examples/.*\.rb}) { |m| m[1] }
25
+ end
26
+
27
+ guard 'yard' do
28
+ watch(%r{app/.+\.rb})
29
+ watch(%r{lib/.+\.rb})
30
+ watch(%r{ext/.+\.c})
31
+ end
data/README.md ADDED
@@ -0,0 +1,31 @@
1
+ # Mongrep
2
+
3
+ This gem provides classes and modules for implementing persistance layers using
4
+ the repository pattern
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'mongrep', '~> 0.2'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ ## Documentation
19
+
20
+ API docs are available at https://rubydocs.info/mongrep
21
+
22
+ ## Development
23
+
24
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
25
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
26
+ prompt that will allow you to experiment.
27
+
28
+ To install this gem onto your local machine, run `bundle exec rake install`. To
29
+ release a new version, update the version number in `version.rb`, and then run
30
+ `bundle exec rake release`, which will create a git tag for the version, push
31
+ git commits and tags and push the gem to rubygems.org.
data/README.md.erb ADDED
@@ -0,0 +1,31 @@
1
+ # Mongrep
2
+
3
+ This gem provides classes and modules for implementing persistance layers using
4
+ the repository pattern
5
+
6
+ ## Installation
7
+
8
+ Add this line to your application's Gemfile:
9
+
10
+ ```ruby
11
+ gem 'mongrep', '~> <%= Mongrep::VERSION.split('.').first(2).join('.') %>'
12
+ ```
13
+
14
+ And then execute:
15
+
16
+ $ bundle
17
+
18
+ ## Documentation
19
+
20
+ API docs are available at https://rubydocs.info/mongrep
21
+
22
+ ## Development
23
+
24
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run
25
+ `rake test` to run the tests. You can also run `bin/console` for an interactive
26
+ prompt that will allow you to experiment.
27
+
28
+ To install this gem onto your local machine, run `bundle exec rake install`. To
29
+ release a new version, update the version number in `version.rb`, and then run
30
+ `bundle exec rake release`, which will create a git tag for the version, push
31
+ git commits and tags and push the gem to rubygems.org.
data/Rakefile ADDED
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+ require 'bundler/gem_tasks'
3
+ require 'rspec/core/rake_task'
4
+ require 'rubocop/rake_task'
5
+ require 'erb'
6
+ require 'yard'
7
+ require 'json'
8
+
9
+ require 'mongrep/version'
10
+
11
+ RSpec::Core::RakeTask.new(:spec)
12
+
13
+ task default: :test
14
+
15
+ task test: [:spec, :rubocop]
16
+
17
+ RuboCop::RakeTask.new
18
+
19
+ Rake::Task['release:guard_clean'].enhance do
20
+ begin
21
+ system('git stash')
22
+ erb = ERB.new(File.read(File.expand_path('../README.md.erb', __FILE__)))
23
+ File.write(File.expand_path('../README.md', __FILE__), erb.result)
24
+ system('git add README.md')
25
+ system('git commit -m "Update gem version in readme"')
26
+ ensure
27
+ system('git stash pop')
28
+ end
29
+ end
data/bin/console ADDED
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'bundler/setup'
5
+ require 'mongrep'
6
+
7
+ # You can add fixtures and/or initialization code here to make experimenting
8
+ # with your gem easier. You can also use a different console, if you like.
9
+
10
+ # (If you use this, don't forget to add pry to your Gemfile!)
11
+ # require 'pry'
12
+ # Pry.start
13
+
14
+ require 'pry'
15
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/core_ext/hash/indifferent_access'
3
+
4
+ module Mongrep
5
+ # Top level namespace for all core extensions
6
+ module CoreExt
7
+ # Core extensions to Hash class
8
+ module Hash
9
+ # Produces a hash containing only given keys which can be in dot
10
+ # notation
11
+ # @param keys [<String, Symbol>] the keys to include in the resulting
12
+ # hash. Note that they are stringified and self will be accessed
13
+ # with indifferent access.
14
+ # @return [{String => Object}] the hash including only the selected
15
+ # stringified keys
16
+ # @example
17
+ # hash = { foo: { bar: 'foobar' }, bar: 'foo' }
18
+ # hash.slice_with_dot_notation('foo.bar')
19
+ # #=> { 'foo.bar' => 'foobar' }
20
+ def slice_with_dot_notation(*keys)
21
+ keys.map(&:to_s).each_with_object(self.class.new) do |key, hash|
22
+ path = key.to_s.split('.')
23
+
24
+ catch :missing_key do
25
+ hash[key] = path.reduce(with_indifferent_access) do |level, part|
26
+ throw :missing_key unless level.key?(part)
27
+ level[part]
28
+ end
29
+ end
30
+ end
31
+ end
32
+
33
+ ::Hash.include(self)
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+ require 'mongo'
3
+
4
+ module Mongrep
5
+ module CoreExt
6
+ # Core extensions to Mongo
7
+ module Mongo
8
+ # Core extensions to Mongo::Error
9
+ module Error
10
+ # Core extensions to Mongo::Error::OperationFailure
11
+ module OperationFailure
12
+ def duplicate_key_error?
13
+ message.start_with?('E11000')
14
+ end
15
+
16
+ ::Mongo::Error::OperationFailure.include(self)
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+ require 'virtus'
3
+
4
+ module Mongrep
5
+ # A mixin providing Virtus.model functionality and a recursive to_h method
6
+ module Model
7
+ class << self
8
+ private
9
+
10
+ # The included method is used here to ensure correct order
11
+ # of inclusion and method overrides
12
+ def included(base)
13
+ base.include(Virtus.model(strict: true))
14
+ base.include(VirtusExtensions)
15
+ base.extend(ClassMethods)
16
+ end
17
+ end
18
+
19
+ # Static methods of Shells
20
+ module Shell
21
+ # @return [String] A human readable string representation of the
22
+ # instance
23
+ def inspect
24
+ instance_vars = instance_variables.map do |var|
25
+ "#{var}=#{instance_variable_get(var).inspect}"
26
+ end
27
+ "#<#{self.class.inspect}:#{object_id} #{instance_vars.join(' ')}>"
28
+ end
29
+
30
+ # Static class methods for Shells
31
+ module ClassMethods
32
+ # @return [String] A human readable string representation of the
33
+ # class instance
34
+ def inspect
35
+ name
36
+ end
37
+ end
38
+ end
39
+
40
+ # Class methods for models
41
+ # TODO: Clean this up
42
+ # @!classmethods
43
+ module ClassMethods
44
+ # Returns a new model that includes just the provided subset of fields
45
+ # from the original model
46
+ # @overload partial(*field_names)
47
+ # @param field_names [Array(Symbol)] a list of fields to include in the
48
+ # resulting model
49
+ # @overload partial(*field_names, nested_fields)
50
+ # @param field_names [Array(Symbol)] a list of fields to include in the
51
+ # resulting model
52
+ # @param nested_fields [Hash(Symbol => Array(Symbol, Hash))]
53
+ # nested fields to include in the resulting model
54
+ # @return [Model] the generated model class
55
+ # @example
56
+ # MyModel.partial(:foo, :bar)
57
+ # @example With nested fields
58
+ # MyModel.partial(:foo, :bar, nested: [:foo, { deep: [:bar] }])
59
+ def partial(*fields)
60
+ class_name = "#{name}::Partial[#{fields.map(&:inspect).join(', ')}]"
61
+ attributes = partial_attributes(*fields)
62
+ Class.new do
63
+ include Model
64
+ include Shell
65
+ extend Shell::ClassMethods
66
+
67
+ define_singleton_method(:name) { class_name }
68
+ attributes.each { |attribute| attribute_set << attribute }
69
+ end
70
+ end
71
+
72
+ private
73
+
74
+ def partial_attributes(*fields)
75
+ field_hash = fields.last.is_a?(Hash) ? fields.pop : {}
76
+
77
+ (fields + field_hash.keys).map do |name|
78
+ attribute = attribute_set[name]
79
+ nested_fields = field_hash[name]
80
+ if nested_fields
81
+ nested_partial_attribute(attribute, *nested_fields)
82
+ else
83
+ attribute
84
+ end
85
+ end
86
+ end
87
+
88
+ def nested_partial_attribute(attribute, *fields)
89
+ type = nested_partial(attribute.type, *fields)
90
+ options = attribute.options.slice(
91
+ :primitive, :default, :strict, :required, :finalize,
92
+ :nullify_blank, :reader, :writer, :name, :coerce
93
+ )
94
+ Virtus::Attribute.build(type, options)
95
+ end
96
+
97
+ def nested_partial(type, *fields)
98
+ case type
99
+ when Virtus::Attribute::Collection::Type
100
+ type.primitive[type.member_type.partial(*fields)]
101
+ when Virtus::Attribute::Hash::Type
102
+ Hash[type.key_type.primitive => type.value_type.partial(*fields)]
103
+ else type.primitive.partial(*fields)
104
+ end
105
+ end
106
+ end
107
+
108
+ # @!parse include VirtusExtensions
109
+
110
+ # Extensions to Virtus models
111
+ module VirtusExtensions
112
+ # Converts the model into a hash. This supports nested models.
113
+ # @return [Hash] A Hash representation of the models attributes
114
+ def to_h
115
+ result = {}
116
+ super.each { |key, value| result[key] = hashify_value(value) }
117
+ result
118
+ end
119
+
120
+ # Checks for equality between self and other
121
+ # @param other [Model] Another Model
122
+ # @return [true] If other has equal attributes
123
+ # @return [false] otherwise
124
+ def ==(other)
125
+ return false unless other.is_a?(self.class)
126
+ to_h == other.to_h
127
+ end
128
+
129
+ private
130
+
131
+ def hashify_value(value)
132
+ case value
133
+ when Array then value.map(&method(:hashify_value))
134
+ when Hash
135
+ result = {}
136
+ value.each { |k, v| result[k] = hashify_value(v) }
137
+ result
138
+ when Model then value.to_h
139
+ else value
140
+ end
141
+ end
142
+ end
143
+ end
144
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+ require 'abstractize'
3
+ require 'active_support/core_ext/hash/indifferent_access'
4
+
5
+ require 'mongrep/model'
6
+
7
+ module Mongrep
8
+ # @abstract The base class for all models
9
+ class MongoModel
10
+ include Abstractize
11
+ include Model
12
+
13
+ define_abstract_method :_id
14
+
15
+ # An alias for #_id
16
+ def id
17
+ _id
18
+ end
19
+
20
+ # Used by Mongo to convert the model into BSON
21
+ # @api private
22
+ def bson_type
23
+ Hash::BSON_TYPE
24
+ end
25
+
26
+ # Used by Mongo to convert the model into BSON
27
+ # @api private
28
+ def to_bson(*args)
29
+ to_h.to_bson(*args)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+ module Mongrep
3
+ # A mongodb query object
4
+ class Query
5
+ # @param query_hash [Hash] A hash representing the query
6
+ # @example
7
+ # Query.new(name: 'test')
8
+ def initialize(query_hash = {})
9
+ @query_hash = query_hash.to_h
10
+ end
11
+
12
+ # Combines two queries by merging their underlying query hashes
13
+ # @param other [Query] The query to combine self with
14
+ # @return [Query] A new Query instance resulting in the combination of
15
+ # self and other
16
+ # @example
17
+ # first = Query.new(name: 'test', value: 5)
18
+ # second = Query.new(value: 6)
19
+ # (first & second).to_h #=> { name: 'test', value: 6 }
20
+ def &(other)
21
+ self.class.new(@query_hash.merge(other.to_h))
22
+ end
23
+
24
+ # Combines two queries by using the MongoDB $or operator
25
+ # @param other [Query] The query to combine self with
26
+ # @return [Query] A new Query instance resulting in the combination of
27
+ # self and other
28
+ # @example
29
+ # first = Query.new(name: 'foo')
30
+ # second = Query.new(name: 'bar')
31
+ # (first | second).to_h
32
+ # #=> { :$or => [{ name: 'foo' }, { name: 'bar' }] }
33
+ def |(other)
34
+ self.class.new(:$or => [@query_hash, other.to_h])
35
+ end
36
+
37
+ # Combines self with the given query hash by merging it to the
38
+ # existing one
39
+ # @param query_hash [Hash] The query hash to merge into the query
40
+ # @return [Query] A new Query resulting in the combination of the
41
+ # given query hash and the existing one
42
+ # @example
43
+ # query.where(name: 'test')
44
+ # @note This is mainly for usage in Repository#find using a block
45
+ # @see #&
46
+ # @see Repository Repository#find
47
+ def where(query_hash)
48
+ self & self.class.new(query_hash)
49
+ end
50
+
51
+ # Combines self with the given query hash by using the MongoDB $or
52
+ # operator
53
+ # @param query_hash [Hash] The query hash to combine with the query
54
+ # @return [Query] A new Query resulting in the combination of the
55
+ # given query hash and the existing one
56
+ # @see #|
57
+ # @example
58
+ # query.where(name: 'foo').or(name: 'bar')
59
+ def or(query_hash)
60
+ self | self.class.new(query_hash)
61
+ end
62
+
63
+ alias and where
64
+
65
+ # @return [Hash] The underlying query hash
66
+ def to_h
67
+ @query_hash
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+ module Mongrep
3
+ # A wrapper around mongo cursors
4
+ class QueryResult
5
+ include Enumerable
6
+
7
+ # The methods to be delegated to the underlying mongo cursor
8
+ # @api private
9
+ DELEGATED_METHODS = %i(limit projection skip sort).freeze
10
+
11
+ attr_reader :model_class
12
+ # @api private
13
+ def initialize(collection_view, model_class)
14
+ @model_class = model_class
15
+ @collection_view = collection_view
16
+ end
17
+
18
+ DELEGATED_METHODS.each do |method|
19
+ aggregation_stage = method == :projection ? :project : method
20
+
21
+ define_method(method) do |param|
22
+ if @collection_view.is_a?(Mongo::Collection::View::Aggregation)
23
+ @collection_view.pipeline << { :"$#{aggregation_stage}" => param }
24
+ else
25
+ @collection_view = @collection_view.public_send(method, param)
26
+ end
27
+
28
+ self
29
+ end
30
+ end
31
+
32
+ # Iterates over the query result
33
+ # @yieldparam item [Model] A model representing a document from the
34
+ # query result
35
+ # @return [void]
36
+ def each
37
+ return enum_for(:each) unless block_given?
38
+
39
+ @collection_view.each do |document|
40
+ yield @model_class.new(document)
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'mongrep/repository'
4
+ module Mongrep
5
+ # A mixin providing overwrites for write methods in read-only repositories
6
+ module ReadOnlyRepository
7
+ # An error signaling that the write operation isn't possible
8
+ class WriteError < ArgumentError; end
9
+
10
+ # @!method insert(*)
11
+ # @raise [WriteError] - Always raises
12
+ # @!method update(*)
13
+ # @raise [WriteError] - Always raises
14
+ # @!method delete(*)
15
+ # @raise [WriteError] - Always raises
16
+ %i(insert update delete).each do |method|
17
+ define_method(method) do |*|
18
+ raise WriteError, 'this repository is read-only'
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+ require 'active_support/inflector'
3
+ require 'active_support/core_ext/hash/except'
4
+ require 'active_support/core_ext/hash/slice'
5
+ require 'mongrep/core_ext/hash'
6
+ require 'abstractize'
7
+ require 'mongrep/query'
8
+ require 'mongrep/query_result'
9
+ require 'mongrep/core_ext/mongo/error/operation_failure'
10
+
11
+ module Mongrep
12
+ # @abstract The base class for all repositories
13
+ class Repository
14
+ include Abstractize
15
+
16
+ # An error signaling that a document could not be found
17
+ class DocumentNotFoundError < RuntimeError; end
18
+ # An error signaling that a model has not been persisted
19
+ class UnpersistedModelError < ArgumentError; end
20
+ # An error signaling that a document already exists
21
+ class DocumentExistsError < ArgumentError; end
22
+
23
+ attr_reader :collection
24
+
25
+ # @param mongo_client [Mongo::Client] The mongodb client to use
26
+ # @param collection_name [String] The name of the collection to use
27
+ # defaults to the demodulized and underscored class name
28
+ def initialize(
29
+ mongo_client,
30
+ collection_name = self.class.name.demodulize.underscore
31
+ )
32
+ @collection = mongo_client[collection_name]
33
+ end
34
+
35
+ # Derives the model class from the class name, requires and returns it
36
+ # @return [Model.class] The model class for this repository
37
+ # @example
38
+ # repository = Shop::ShoppingCarts.new(mongo_client)
39
+ # repository.model_class #=> Shop::Models::ShoppingCart
40
+ def model_class
41
+ model_name = self.class.name.demodulize.singularize
42
+ Mongrep.models_namespace.const_get(model_name)
43
+ end
44
+
45
+ # Finds documents matching the given query
46
+ # @overload find(query)
47
+ # @param query [Hash, Query] The mongodb query to perform
48
+ # @overload find(query, options)
49
+ # @param query [Hash, Query] The mongodb query to perform
50
+ # @param options [Hash] Options to pass to the query
51
+ # @overload find
52
+ # @yieldparam query [Query] A new query
53
+ # @yieldreturn [Query] The query to be used
54
+ # @overload find(query)
55
+ # @param query [Hash, Query] The initial query
56
+ # @yieldparam query [Query] The query object
57
+ # @yieldreturn [Query] The final query to be used
58
+ # @overload find(query, options)
59
+ # @param query [Hash, Query] The initial query
60
+ # @param options [Hash] Options to pass to the query
61
+ # @yieldparam query [Query] The query object
62
+ # @yieldreturn [Query] The final query to be used
63
+ # @return [QueryResult<Model>] An enumerable query result
64
+ # @example With query hash
65
+ # result = repository.find(name: 'test')
66
+ # @example With Query object
67
+ # repeating_query = Query.new(name: 'test')
68
+ # result = repository.find(repeating_query)
69
+ # @example With code block
70
+ # result = repository.find do |query|
71
+ # query.where(name: 'test 1').or(name: 'test 2')
72
+ # end
73
+ # @example With query hash and options
74
+ # result = repository.find({ name: 'test' }, limit: 1)
75
+ # @see Query
76
+ # @see QueryResult
77
+ def find(query = {}, options = {})
78
+ query_object = query.is_a?(Hash) ? Query.new(query) : query
79
+ query_object = yield(query_object) if block_given?
80
+ execute_query(query_object, options)
81
+ end
82
+
83
+ # Finds a single document matching the given query
84
+ # @overload find_one(query)
85
+ # @param query [Hash, Query] The mongodb query to perform
86
+ # @overload find_one(query, options)
87
+ # @param query [Hash, Query] The mongodb query to perform
88
+ # @param options [Hash] Options to pass to the query
89
+ # @overload find_one
90
+ # @yieldparam query [Query] A new query
91
+ # @yieldreturn [Query] The query to be used
92
+ # @overload find_one(query)
93
+ # @param query [Hash, Query] The initial query
94
+ # @yieldparam query [Query] The query object
95
+ # @yieldreturn [Query] The final query to be used
96
+ # @overload find_one(query, options)
97
+ # @param query [Hash, Query] The initial query
98
+ # @param options [Hash] Options to pass to the query
99
+ # @yieldparam query [Query] The query object
100
+ # @yieldreturn [Query] The final query to be used
101
+ # @raise [DocumentNotFoundError] if no matching document could be found
102
+ # @return [Model] The single model instance representing the document
103
+ # matching the query
104
+ def find_one(query = {}, options = {}, &block)
105
+ # TODO: Pass some context to DocumentNotFoundError
106
+ find(query, options, &block).first || raise(DocumentNotFoundError)
107
+ end
108
+
109
+ # Inserts a document into the database
110
+ # @param model [Model] The model representing the document to be inserted
111
+ # @return [Mongo::Operation::Write::Insert::Result] The result of the
112
+ # insert operation
113
+ def insert(model)
114
+ collection.insert_one(model.to_h)
115
+ rescue Mongo::Error::OperationFailure => error
116
+ # TODO: Pass relevant info to DocumentExistsError message
117
+ raise(error.duplicate_key_error? ? DocumentExistsError : error)
118
+ end
119
+
120
+ # Update an existing document in the database
121
+ # @param model [Model] The model representing the document to be updated
122
+ # @option options [Array<String>] :fields The specific fields to update.
123
+ # If this option is omitted the whole document is updated
124
+ # @raise [UnpersistedModelError] if the model is not persisted
125
+ # (has no value for _id)
126
+ # @raise [DocumentNotFoundError] if nothing was updated
127
+ # (no document found for _id)
128
+ # @return [Mongo::Operation::Write::Update::Result] The result of the
129
+ # update operation
130
+ def update(model, options = {})
131
+ check_persistence!(model)
132
+ result = collection.update_one(
133
+ id_query(model),
134
+ update_hash(model, options[:fields])
135
+ )
136
+ # TODO: Pass some context to DocumentNotFoundError
137
+ raise(DocumentNotFoundError) if result.documents.first['n'].zero?
138
+ result
139
+ end
140
+
141
+ # TODO: implement upsert
142
+
143
+ # Delete an existing document from the database
144
+ # @param model [Model] The model representing the document to be updated
145
+ # @raise [UnpersistedModelError] if the model is not persisted
146
+ # (has no value for _id)
147
+ # @raise [DocumentNotFoundError] if nothing was deleted
148
+ # (no document found for _id)
149
+ # @return [Mongo::Operation::Write::Delete::Result] The result of the
150
+ # delete operation
151
+ def delete(model)
152
+ check_persistence!(model)
153
+ result = collection.delete_one(id_query(model))
154
+ # TODO: Pass some context to DocumentNotFoundError
155
+ raise(DocumentNotFoundError) if result.documents.first['n'].zero?
156
+ result
157
+ end
158
+
159
+ # Get a distinct list of values for the given field over all documents
160
+ # in the collection.
161
+ # @param field [Symbol, String] The field or dot notated path to the
162
+ # field
163
+ # @return [Array] An array with the distinct values
164
+ def distinct(field)
165
+ collection.distinct(field)
166
+ end
167
+
168
+ private
169
+
170
+ def update_hash(model, fields_to_set = nil)
171
+ model_fields = model.to_h.except(:_id)
172
+ return model_fields unless fields_to_set
173
+ { :$set => model_fields.slice_with_dot_notation(*fields_to_set) }
174
+ end
175
+
176
+ def id_query(model)
177
+ { _id: model._id }
178
+ end
179
+
180
+ def execute_query(query_object, options)
181
+ check_query_type!(query_object)
182
+ QueryResult.new(collection.find(query_object.to_h, options), model_class)
183
+ end
184
+
185
+ def check_persistence!(model)
186
+ return if model._id
187
+ raise UnpersistedModelError, 'model is not yet persisted'
188
+ end
189
+
190
+ protected
191
+
192
+ def check_query_type!(query_object)
193
+ return if query_object.is_a?(Query)
194
+ raise ArgumentError, 'Invalid type for query ' \
195
+ "(#{query_object.class.name})"
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+ module Mongrep
3
+ # The current version of this gem
4
+ VERSION = '0.2.0'
5
+ end
data/lib/mongrep.rb ADDED
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ require 'mongrep/version'
3
+
4
+ # The top level namespace
5
+ module Mongrep
6
+ # An error signaling an error with the configuration of the gem
7
+ class ConfigurationError < RuntimeError; end
8
+
9
+ module_function
10
+
11
+ # @overload models_namespace
12
+ # Get the namespace where models are defined
13
+ # @overload models_namespace(namespace)
14
+ # Set the namespace where models are defined
15
+ # @param namespace [Module] The namespace module to be set
16
+ # @return [Module] the models namespace
17
+ def models_namespace(namespace = nil)
18
+ unless namespace || @models_namespace
19
+ raise ConfigurationError, 'models namespace is unset'
20
+ end
21
+
22
+ namespace ? @models_namespace = namespace : @models_namespace
23
+ end
24
+ end
data/mongrep.gemspec ADDED
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'mongrep/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'mongrep'
8
+ spec.version = Mongrep::VERSION
9
+ spec.authors = ['Joakim Reinert']
10
+ spec.email = ['reinert@meso.net']
11
+
12
+ spec.summary =
13
+ 'A library for utilizing the repository pattern for MongoDB'
14
+ spec.description =
15
+ 'Mongrep provides base classes and modules for implementing persistance ' \
16
+ 'layers for MongoDB using the repository pattern'
17
+ spec.homepage = 'https://github.com/meso-unimpressed/mongrep'
18
+
19
+ spec.files = `git ls-files -z`.split("\x0").reject do |f|
20
+ f.match(%r{^(test|spec|features)/})
21
+ end
22
+
23
+ spec.bindir = 'exe'
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ['lib']
26
+
27
+ spec.add_development_dependency 'bundler', '~> 1.1'
28
+ spec.add_development_dependency 'rake', '~> 11.2'
29
+ spec.add_development_dependency 'rspec', '~> 3.5'
30
+ spec.add_development_dependency 'rubocop', '~> 0.4'
31
+ spec.add_development_dependency 'rubocop-rspec', '~> 1.7'
32
+ spec.add_development_dependency 'pry', '~> 0.1'
33
+ spec.add_development_dependency 'pry-byebug', '~> 3.4'
34
+ spec.add_development_dependency 'simplecov', '~> 0.1'
35
+ spec.add_development_dependency 'guard', '~> 2.1'
36
+ spec.add_development_dependency 'guard-rspec', '~> 4.7'
37
+ spec.add_development_dependency 'guard-yard', '~> 2.1'
38
+ spec.add_development_dependency 'libnotify', '~> 0.9'
39
+ spec.add_development_dependency 'yard', '~> 0.9'
40
+ spec.add_development_dependency 'yard-classmethods', '~> 1.0'
41
+ spec.add_development_dependency 'factory_girl', '~> 4.7'
42
+
43
+ spec.add_dependency 'abstractize', '~> 0.1'
44
+ spec.add_dependency 'mongo', '~> 2.3'
45
+ spec.add_dependency 'virtus', '~> 1.0'
46
+ spec.add_dependency 'activesupport', '~> 5.0'
47
+ end
metadata ADDED
@@ -0,0 +1,336 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: mongrep
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Joakim Reinert
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2016-11-10 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.1'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.1'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '11.2'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '11.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '3.5'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '3.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rubocop
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '0.4'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '0.4'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rubocop-rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.7'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.7'
83
+ - !ruby/object:Gem::Dependency
84
+ name: pry
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: '0.1'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: '0.1'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry-byebug
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: '3.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: '3.4'
111
+ - !ruby/object:Gem::Dependency
112
+ name: simplecov
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: '0.1'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: '0.1'
125
+ - !ruby/object:Gem::Dependency
126
+ name: guard
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '2.1'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '2.1'
139
+ - !ruby/object:Gem::Dependency
140
+ name: guard-rspec
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: '4.7'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: '4.7'
153
+ - !ruby/object:Gem::Dependency
154
+ name: guard-yard
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '2.1'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '2.1'
167
+ - !ruby/object:Gem::Dependency
168
+ name: libnotify
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: '0.9'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: '0.9'
181
+ - !ruby/object:Gem::Dependency
182
+ name: yard
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '0.9'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - "~>"
193
+ - !ruby/object:Gem::Version
194
+ version: '0.9'
195
+ - !ruby/object:Gem::Dependency
196
+ name: yard-classmethods
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - "~>"
200
+ - !ruby/object:Gem::Version
201
+ version: '1.0'
202
+ type: :development
203
+ prerelease: false
204
+ version_requirements: !ruby/object:Gem::Requirement
205
+ requirements:
206
+ - - "~>"
207
+ - !ruby/object:Gem::Version
208
+ version: '1.0'
209
+ - !ruby/object:Gem::Dependency
210
+ name: factory_girl
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - "~>"
214
+ - !ruby/object:Gem::Version
215
+ version: '4.7'
216
+ type: :development
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - "~>"
221
+ - !ruby/object:Gem::Version
222
+ version: '4.7'
223
+ - !ruby/object:Gem::Dependency
224
+ name: abstractize
225
+ requirement: !ruby/object:Gem::Requirement
226
+ requirements:
227
+ - - "~>"
228
+ - !ruby/object:Gem::Version
229
+ version: '0.1'
230
+ type: :runtime
231
+ prerelease: false
232
+ version_requirements: !ruby/object:Gem::Requirement
233
+ requirements:
234
+ - - "~>"
235
+ - !ruby/object:Gem::Version
236
+ version: '0.1'
237
+ - !ruby/object:Gem::Dependency
238
+ name: mongo
239
+ requirement: !ruby/object:Gem::Requirement
240
+ requirements:
241
+ - - "~>"
242
+ - !ruby/object:Gem::Version
243
+ version: '2.3'
244
+ type: :runtime
245
+ prerelease: false
246
+ version_requirements: !ruby/object:Gem::Requirement
247
+ requirements:
248
+ - - "~>"
249
+ - !ruby/object:Gem::Version
250
+ version: '2.3'
251
+ - !ruby/object:Gem::Dependency
252
+ name: virtus
253
+ requirement: !ruby/object:Gem::Requirement
254
+ requirements:
255
+ - - "~>"
256
+ - !ruby/object:Gem::Version
257
+ version: '1.0'
258
+ type: :runtime
259
+ prerelease: false
260
+ version_requirements: !ruby/object:Gem::Requirement
261
+ requirements:
262
+ - - "~>"
263
+ - !ruby/object:Gem::Version
264
+ version: '1.0'
265
+ - !ruby/object:Gem::Dependency
266
+ name: activesupport
267
+ requirement: !ruby/object:Gem::Requirement
268
+ requirements:
269
+ - - "~>"
270
+ - !ruby/object:Gem::Version
271
+ version: '5.0'
272
+ type: :runtime
273
+ prerelease: false
274
+ version_requirements: !ruby/object:Gem::Requirement
275
+ requirements:
276
+ - - "~>"
277
+ - !ruby/object:Gem::Version
278
+ version: '5.0'
279
+ description: Mongrep provides base classes and modules for implementing persistance
280
+ layers for MongoDB using the repository pattern
281
+ email:
282
+ - reinert@meso.net
283
+ executables: []
284
+ extensions: []
285
+ extra_rdoc_files: []
286
+ files:
287
+ - ".editorconfig"
288
+ - ".gitignore"
289
+ - ".rspec"
290
+ - ".rubocop.yml"
291
+ - ".ruby-version"
292
+ - ".simplecov"
293
+ - ".travis.yml"
294
+ - ".yardopts"
295
+ - Gemfile
296
+ - Guardfile
297
+ - README.md
298
+ - README.md.erb
299
+ - Rakefile
300
+ - bin/console
301
+ - bin/setup
302
+ - lib/mongrep.rb
303
+ - lib/mongrep/core_ext/hash.rb
304
+ - lib/mongrep/core_ext/mongo/error/operation_failure.rb
305
+ - lib/mongrep/model.rb
306
+ - lib/mongrep/mongo_model.rb
307
+ - lib/mongrep/query.rb
308
+ - lib/mongrep/query_result.rb
309
+ - lib/mongrep/read_only_repository.rb
310
+ - lib/mongrep/repository.rb
311
+ - lib/mongrep/version.rb
312
+ - mongrep.gemspec
313
+ homepage: https://github.com/meso-unimpressed/mongrep
314
+ licenses: []
315
+ metadata: {}
316
+ post_install_message:
317
+ rdoc_options: []
318
+ require_paths:
319
+ - lib
320
+ required_ruby_version: !ruby/object:Gem::Requirement
321
+ requirements:
322
+ - - ">="
323
+ - !ruby/object:Gem::Version
324
+ version: '0'
325
+ required_rubygems_version: !ruby/object:Gem::Requirement
326
+ requirements:
327
+ - - ">="
328
+ - !ruby/object:Gem::Version
329
+ version: '0'
330
+ requirements: []
331
+ rubyforge_project:
332
+ rubygems_version: 2.5.1
333
+ signing_key:
334
+ specification_version: 4
335
+ summary: A library for utilizing the repository pattern for MongoDB
336
+ test_files: []