mongrep 0.2.0

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 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: []