├── .codeclimate.yml ├── .gitignore ├── .rspec ├── .rubocop.yml ├── .semaphore └── semaphore.yml ├── Appraisals ├── CHANGELOG.md ├── Gemfile ├── Guardfile ├── LICENSE.txt ├── README.md ├── Rakefile ├── gemfiles ├── .bundle │ └── config ├── mongoid_4.gemfile ├── mongoid_5.gemfile ├── mongoid_6.gemfile ├── mongoid_7.gemfile └── mongoid_8.gemfile ├── lib ├── mongoid-embedded-errors.rb └── mongoid │ ├── embedded_errors.rb │ └── embedded_errors │ ├── embedded_in.rb │ └── version.rb ├── mongoid-embedded-errors.gemspec └── spec ├── lib └── mongoid │ └── embedded_errors_spec.rb ├── spec_helper.rb └── support ├── database_cleaner.rb ├── models ├── annotation.rb ├── article.rb ├── page.rb └── section.rb └── mongoid.yml /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "2" 3 | 4 | plugins: 5 | rubocop: 6 | enabled: true 7 | bundler-audit: 8 | enabled: true 9 | fixme: 10 | enabled: true 11 | duplication: 12 | enabled: true 13 | reek: 14 | enabled: true 15 | git-legal: 16 | enabled: true 17 | 18 | config: 19 | languages: 20 | - ruby 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | doc 4 | tmp 5 | pkg 6 | *.gem 7 | *.pid 8 | coverage 9 | coverage.data 10 | build/* 11 | *.pbxuser 12 | *.mode1v3 13 | .svn 14 | profile 15 | .console_history 16 | .sass-cache/* 17 | .rake_tasks~ 18 | *.log.lck 19 | solr/ 20 | bin/ 21 | *.lock 22 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | --format documentation 4 | -------------------------------------------------------------------------------- /.rubocop.yml: -------------------------------------------------------------------------------- 1 | # Use RuboCop RSpec 2 | require: rubocop-rspec 3 | 4 | # Common configuration. 5 | AllCops: 6 | # Cop names are not displayed in offense messages by default. Change behavior 7 | # by overriding DisplayCopNames, or by giving the -D/--display-cop-names 8 | # option. 9 | DisplayCopNames: true 10 | Exclude: 11 | - Guardfile 12 | - gemfiles/**/* 13 | - bin/* 14 | - vendor/bundle/**/* 15 | - tmp/**/* 16 | NewCops: enable 17 | 18 | Naming/FileName: 19 | Exclude: 20 | - Appraisals 21 | 22 | Layout/SpaceInsideHashLiteralBraces: 23 | EnforcedStyle: no_space 24 | EnforcedStyleForEmptyBraces: no_space 25 | SupportedStyles: 26 | - space 27 | - no_space 28 | 29 | Metrics/MethodLength: 30 | CountComments: false # count full line comments? 31 | Max: 40 32 | 33 | Metrics/AbcSize: 34 | Max: 40 35 | 36 | Layout/LineLength: 37 | Max: 120 38 | 39 | Metrics/BlockLength: 40 | Max: 50 41 | 42 | Style/Documentation: 43 | Description: 'Document classes and non-namespace modules.' 44 | Enabled: false 45 | 46 | Style/AutoResourceCleanup: 47 | Description: 'Suggests the usage of an auto resource cleanup version of a method (if available).' 48 | Enabled: true 49 | 50 | Style/CollectionMethods: 51 | Description: 'Preferred collection methods.' 52 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#map-find-select-reduce-size' 53 | Enabled: true 54 | 55 | Style/MethodCalledOnDoEndBlock: 56 | Description: 'Avoid chaining a method call on a do...end block.' 57 | StyleGuide: 'https://github.com/bbatsov/ruby-style-guide#single-line-blocks' 58 | Enabled: true 59 | 60 | Layout/ExtraSpacing: 61 | Description: 'Do not use unnecessary spacing.' 62 | Enabled: true 63 | 64 | Style/ClassAndModuleChildren: 65 | # Checks the style of children definitions at classes and modules. 66 | # 67 | # Basically there are two different styles: 68 | # 69 | # `nested` - have each child on a separate line 70 | # class Foo 71 | # class Bar 72 | # end 73 | # end 74 | # 75 | # `compact` - combine definitions as much as possible 76 | # class Foo::Bar 77 | # end 78 | # 79 | # The compact style is only forced, for classes / modules with one child. 80 | EnforcedStyle: compact 81 | SupportedStyles: 82 | - nested 83 | - compact 84 | -------------------------------------------------------------------------------- /.semaphore/semaphore.yml: -------------------------------------------------------------------------------- 1 | version: v1.0 2 | name: mongoid-embedded-errors 3 | agent: 4 | machine: 5 | type: e1-standard-2 6 | os_image: ubuntu1804 7 | 8 | blocks: 9 | - name: Code Climate 10 | task: 11 | jobs: 12 | - name: Code Climate 13 | commands: 14 | - checkout 15 | - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH,gems-master 16 | - bundle install --path vendor/bundle 17 | - cache store gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock) vendor/bundle 18 | - docker run --interactive --tty --rm --env CODECLIMATE_CODE="$PWD" --volume "$PWD":/code --volume /tmp/cc:/tmp/cc --volume /var/run/docker.sock:/var/run/docker.sock codeclimate/codeclimate analyze 19 | 20 | - name: Unit tests 21 | task: 22 | jobs: 23 | - name: Appraisal 24 | commands: 25 | - checkout 26 | - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH,gems-master 27 | - bundle install --path vendor/bundle 28 | - cache store gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock) vendor/bundle 29 | - cache restore appraisal-$SEMAPHORE_GIT_BRANCH-$(checksum gemfiles/*.gemfile.lock),appraisal-$SEMAPHORE_GIT_BRANCH,appraisal-master 30 | - bundle exec appraisal 31 | - cache store appraisal-$SEMAPHORE_GIT_BRANCH-$(checksum gemfiles/*.gemfile.lock) vendor/bundle 32 | - sem-service start mongodb 33 | - bundle exec appraisal rake test 34 | 35 | - name: Mutant 36 | task: 37 | jobs: 38 | - name: Mutant 39 | commands: 40 | - checkout 41 | - cache restore gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock),gems-$SEMAPHORE_GIT_BRANCH,gems-master 42 | - bundle install --path vendor/bundle 43 | - cache store gems-$SEMAPHORE_GIT_BRANCH-$(checksum Gemfile.lock) vendor/bundle 44 | - sem-service start mongodb 45 | - MUTANT=true bundle exec mutant --since "$SEMAPHORE_GIT_SHA~1" --include lib --require mongoid --use rspec "Mongoid::EmbeddedErrors*" 46 | 47 | -------------------------------------------------------------------------------- /Appraisals: -------------------------------------------------------------------------------- 1 | appraise 'mongoid-7' do 2 | gem 'mongoid', '~> 7.0' 3 | end 4 | 5 | appraise 'mongoid-8' do 6 | gem 'mongoid', '~> 8.0' 7 | end 8 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | All notable changes to this project will be documented in this file. 3 | 4 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 5 | 6 | ## [Unreleased] 7 | - TODO 8 | 9 | ## [v4.0.0] - 2022-08-31 10 | - Fix rails 6 active model error deprecations (#22) 11 | - Added Mongoid 8 support 12 | - Dropped support for Mongoid 4, 5 and 6 13 | 14 | ## [v3.0.1] - 2020-09-29 15 | ### Added 16 | - Started 🔎 tracking changes in a changelog! 17 | - Mongoid 7 support 18 | - Ruby 2.4 support by using `send` instead of `public_send` [#18] 19 | 20 | ### Removed 21 | - Mongoid 3 support 22 | 23 | ### Fixed 24 | - Updated style to fix Rubocop issues (as per included .rubocop.yml configuration file) 25 | - Removed .lock files as these are not supposed to be included with gem source code 26 | 27 | ## [v3.0.0] - 2020-09-29 [YANKED] 28 | - Yanked due to wrong dependencies in gemspec. 29 | 30 | [Unreleased]: https://github.com/glooko/mongoid-embedded-errors/compare/v4.0.0...HEAD 31 | [v4.0.0]: https://github.com/glooko/mongoid-embedded-errors/compare/v3.0.1...v4.0.0 32 | [v3.0.1]: https://github.com/glooko/mongoid-embedded-errors/compare/v3.0.0...v3.0.1 33 | [v3.0.0]: https://github.com/glooko/mongoid-embedded-errors/compare/f1ce0d8ed140de86c894b2fad7ad197504fefd5a...v3.0.0 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in mongoid-embedded-errors.gemspec 4 | gemspec 5 | 6 | group :development do 7 | gem 'rake' 8 | end 9 | 10 | group :test do 11 | gem 'appraisal' 12 | gem 'database_cleaner-mongoid', '~> 2.0', '>= 2.0.1' 13 | gem 'guard-rspec' 14 | gem 'mutant-rspec' 15 | gem 'rspec' 16 | end 17 | -------------------------------------------------------------------------------- /Guardfile: -------------------------------------------------------------------------------- 1 | # A sample Guardfile 2 | # More info at https://github.com/guard/guard#readme 3 | 4 | ## Uncomment and set this to only include directories you want to watch 5 | # directories %w(app lib config test spec features) \ 6 | # .select{|d| Dir.exists?(d) ? d : UI.warning("Directory #{d} does not exist")} 7 | 8 | ## Note: if you are using the `directories` clause above and you are not 9 | ## watching the project directory ('.'), then you will want to move 10 | ## the Guardfile to a watched dir and symlink it back, e.g. 11 | # 12 | # $ mkdir config 13 | # $ mv Guardfile config/ 14 | # $ ln -s config/Guardfile . 15 | # 16 | # and, you'll have to watch "config/Guardfile" instead of "Guardfile" 17 | 18 | # Note: The cmd option is now required due to the increasing number of ways 19 | # rspec may be run, below are examples of the most common uses. 20 | # * bundler: 'bundle exec rspec' 21 | # * bundler binstubs: 'bin/rspec' 22 | # * spring: 'bin/rspec' (This will use spring if running and you have 23 | # installed the spring binstubs per the docs) 24 | # * zeus: 'zeus rspec' (requires the server to be started separately) 25 | # * 'just' rspec: 'rspec' 26 | 27 | guard :rspec, cmd: "bundle exec rspec" do 28 | require "guard/rspec/dsl" 29 | dsl = Guard::RSpec::Dsl.new(self) 30 | 31 | # Feel free to open issues for suggestions and improvements 32 | 33 | # RSpec files 34 | rspec = dsl.rspec 35 | watch(rspec.spec_helper) { rspec.spec_dir } 36 | watch(rspec.spec_support) { rspec.spec_dir } 37 | watch(rspec.spec_files) 38 | 39 | # Ruby files 40 | ruby = dsl.ruby 41 | dsl.watch_spec_files_for(ruby.lib_files) 42 | end 43 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Mark Bates 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Mongoid::EmbeddedErrors 2 | [![Code Climate](https://codeclimate.com/github/glooko/mongoid-embedded-errors/badges/gpa.svg)](https://codeclimate.com/github/glooko/mongoid-embedded-errors) 3 | 4 | Easily bubble up errors from embedded documents in Mongoid and newer. 5 | 6 | ## Installation 7 | 8 | Add this line to your application's Gemfile: 9 | 10 | gem 'mongoid-embedded-errors' 11 | 12 | And then execute: 13 | 14 | $ bundle 15 | 16 | Or install it yourself as: 17 | 18 | $ gem install mongoid-embedded-errors 19 | 20 | ## Usage 21 | 22 | Embedded documents in Mongoid can be really useful. However, when one of those embedded documents is invalid, Mongoid spits up completely useless errors. 23 | 24 | Let's look an example. Here we have an `Article` which `embeds_many :pages`. A `Page` `embeds_many :sections`. 25 | 26 | ```ruby 27 | class Article 28 | include Mongoid::Document 29 | 30 | field :name, type: String 31 | field :summary, type: String 32 | validates :name, presence: true 33 | validates :summary, presence: true 34 | 35 | embeds_many :pages 36 | end 37 | 38 | class Page 39 | include Mongoid::Document 40 | 41 | field :title, type: String 42 | validates :title, presence: true 43 | 44 | embedded_in :article, inverse_of: :pages 45 | embeds_many :sections 46 | end 47 | 48 | class Section 49 | include Mongoid::Document 50 | 51 | field :header, type: String 52 | field :body, type: String 53 | validates :header, presence: true 54 | 55 | embedded_in :page, inverse_of: :sections 56 | end 57 | ``` 58 | 59 | If we were to create an invalid `Article` with an invalid `Page` and tried to validate it the errors we see would not be very helpful: 60 | 61 | ```ruby 62 | article = Article.new(pages: [Page.new]) 63 | article.valid? # => false 64 | 65 | article.errors.messages 66 | # => {:name=>["can't be blank"], :summary=>["can't be blank"], :pages=>["is invalid"]} 67 | ``` 68 | 69 | Why was the `Page` invalid? Who knows! But, if we include the `Mongoid::EmbeddedErrors` module we get much better error messaging: 70 | 71 | ```ruby 72 | class Article 73 | include Mongoid::Document 74 | include Mongoid::EmbeddedErrors 75 | 76 | field :name, type: String 77 | field :summary, type: String 78 | validates :name, presence: true 79 | validates :summary, presence: true 80 | 81 | embeds_many :pages 82 | end 83 | 84 | article = Article.new(pages: [Page.new(sections: [Section.new])]) 85 | article.valid? # => false 86 | 87 | article.errors.messages 88 | { 89 | :name => ["can't be blank"], 90 | :summary => ["can't be blank"], 91 | :"pages[0].title" => ["can't be blank"], 92 | :"pages[0].sections[0].header" => ["can't be blank"] 93 | } 94 | ``` 95 | 96 | Now, isn't that much nicer? Yeah, I think so to. 97 | 98 | ## Contributing 99 | 100 | 1. Fork it 101 | 2. Create your feature branch (`git checkout -b my-new-feature`) 102 | 3. Commit your changes (`git commit -am 'Add some feature'`) 103 | 4. Push to the branch (`git push origin my-new-feature`) 104 | 5. Create new Pull Request 105 | 106 | ## Contributors 107 | 108 | [See here](https://github.com/glooko/mongoid-embedded-errors/graphs/contributors) 109 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'rubygems' 4 | require 'bundler/setup' 5 | 6 | Bundler::GemHelper.install_tasks 7 | 8 | desc 'Run tests' 9 | task default: [:test] 10 | 11 | desc 'Run tests' 12 | task spec: [:test] 13 | 14 | desc 'Run tests' 15 | task(:test) { system 'bundle exec rspec' } 16 | -------------------------------------------------------------------------------- /gemfiles/.bundle/config: -------------------------------------------------------------------------------- 1 | --- 2 | BUNDLE_RETRY: "1" 3 | -------------------------------------------------------------------------------- /gemfiles/mongoid_4.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mongoid", "~> 4.0" 6 | 7 | group :development do 8 | gem "appraisal" 9 | gem "database_cleaner" 10 | gem "guard-rspec" 11 | gem "mutant-rspec" 12 | gem "rake" 13 | gem "rspec" 14 | end 15 | 16 | gemspec path: "../" 17 | -------------------------------------------------------------------------------- /gemfiles/mongoid_5.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mongoid", "~> 5.0" 6 | 7 | group :development do 8 | gem "appraisal" 9 | gem "database_cleaner" 10 | gem "guard-rspec" 11 | gem "mutant-rspec" 12 | gem "rake" 13 | gem "rspec" 14 | end 15 | 16 | gemspec path: "../" 17 | -------------------------------------------------------------------------------- /gemfiles/mongoid_6.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mongoid", "~> 6.0" 6 | 7 | group :development do 8 | gem "appraisal" 9 | gem "database_cleaner" 10 | gem "guard-rspec" 11 | gem "mutant-rspec" 12 | gem "rake" 13 | gem "rspec" 14 | end 15 | 16 | gemspec path: "../" 17 | -------------------------------------------------------------------------------- /gemfiles/mongoid_7.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mongoid", "~> 7.0" 6 | 7 | group :development do 8 | gem "rake" 9 | end 10 | 11 | group :test do 12 | gem "appraisal" 13 | gem "database_cleaner-mongoid", "~> 2.0", ">= 2.0.1" 14 | gem "guard-rspec" 15 | gem "mutant-rspec" 16 | gem "rspec" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /gemfiles/mongoid_8.gemfile: -------------------------------------------------------------------------------- 1 | # This file was generated by Appraisal 2 | 3 | source "https://rubygems.org" 4 | 5 | gem "mongoid", "~> 8.0" 6 | 7 | group :development do 8 | gem "rake" 9 | end 10 | 11 | group :test do 12 | gem "appraisal" 13 | gem "database_cleaner-mongoid", "~> 2.0", ">= 2.0.1" 14 | gem "guard-rspec" 15 | gem "mutant-rspec" 16 | gem "rspec" 17 | end 18 | 19 | gemspec path: "../" 20 | -------------------------------------------------------------------------------- /lib/mongoid-embedded-errors.rb: -------------------------------------------------------------------------------- 1 | # rubocop:disable Naming/FileName 2 | require 'mongoid/embedded_errors' 3 | # rubocop:enable Naming/FileName 4 | -------------------------------------------------------------------------------- /lib/mongoid/embedded_errors.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'mongoid' 4 | require 'mongoid/embedded_errors/version' 5 | require 'mongoid/embedded_errors/embedded_in' 6 | 7 | module Mongoid::EmbeddedErrors 8 | def self.included(klass) 9 | return if klass.instance_methods.include?(:errors_without_embedded_errors) 10 | 11 | klass.send :alias_method, :errors_without_embedded_errors, :errors 12 | klass.send :alias_method, :errors, :errors_with_embedded_errors 13 | end 14 | 15 | def errors_with_embedded_errors 16 | errors_without_embedded_errors.tap do |errs| 17 | embedded_relations.each do |name, metadata| 18 | # name is something like pages or sections 19 | # if there is an 'is invalid' message for the relation then let's work it: 20 | next unless Array(public_send(name)).any? { |doc| doc.errors.any? } 21 | 22 | # first delete the generic 'is invalid' error for the relation 23 | errs.delete name.to_sym, :invalid 24 | errs.delete name.to_sym if errs[name].empty? 25 | 26 | # next, loop through each of the relations (pages, sections, etc...) 27 | [public_send(name)].flatten.reject(&:nil?).each_with_index do |rel, i| 28 | next unless rel.errors.any? 29 | 30 | # get each of their individual message and add them to the parent's errors: 31 | rel.errors.each do |error| 32 | attribute = error.attribute 33 | message = error.message 34 | relation = if Gem::Version.new(Mongoid::VERSION) >= Gem::Version.new('7.0.0') 35 | metadata.class 36 | else 37 | metadata.relation 38 | end 39 | key = if relation.equal? EMBEDS_MANY 40 | "#{name}[#{i}].#{attribute}" 41 | else 42 | "#{name}.#{attribute}" 43 | end.to_sym 44 | errs.delete(key) 45 | errs.add key, message if message.present? 46 | end 47 | end 48 | end 49 | end 50 | end 51 | end 52 | -------------------------------------------------------------------------------- /lib/mongoid/embedded_errors/embedded_in.rb: -------------------------------------------------------------------------------- 1 | if Gem::Version.new(Mongoid::VERSION) >= Gem::Version.new('7.0.0') 2 | require 'mongoid/association/embedded/embedded_in' 3 | ASSOCIATION = Mongoid::Association::Macros::ClassMethods 4 | EMBEDS_MANY = Mongoid::Association::Embedded::EmbedsMany 5 | else 6 | require 'mongoid/relations/embedded/in' 7 | ASSOCIATION = Mongoid::Relations::Macros::ClassMethods 8 | EMBEDS_MANY = Mongoid::Relations::Embedded::Many 9 | end 10 | 11 | module ASSOCIATION 12 | alias embedded_in_without_embedded_errors embedded_in 13 | def embedded_in(*args) 14 | relation = embedded_in_without_embedded_errors(*args) 15 | send(:include, Mongoid::EmbeddedErrors) 16 | relation 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /lib/mongoid/embedded_errors/version.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | module Mongoid; end 4 | module Mongoid::EmbeddedErrors 5 | VERSION = '4.0.0' 6 | end 7 | -------------------------------------------------------------------------------- /mongoid-embedded-errors.gemspec: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | lib = File.expand_path('lib', __dir__) 4 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 5 | require 'mongoid/embedded_errors/version' 6 | 7 | Gem::Specification.new do |gem| 8 | gem.name = 'mongoid-embedded-errors' 9 | gem.version = Mongoid::EmbeddedErrors::VERSION.dup 10 | gem.authors = ['Mark Bates', 'Kristijan Novoselić'] 11 | gem.email = ['mark@markbates.com', 'kristijan@glooko.com'] 12 | gem.description = 'Embedded documents in Mongoid can be really useful. '\ 13 | 'However, when one of those embedded documents is '\ 14 | 'invalid, Mongoid does not say which validation has '\ 15 | 'failed. Instead of just saying that an embedded '\ 16 | 'document is invalid, this gem modifies Mongoid '\ 17 | 'behavior so it explicitly provides validation errors '\ 18 | 'on a per-field basis for embedded documents, the '\ 19 | 'same way it does for parent documents.' 20 | gem.summary = 'Easily bubble up errors from embedded '\ 21 | 'documents in Mongoid.' 22 | gem.homepage = 'https://github.com/glooko/mongoid-embedded-errors' 23 | gem.licenses = ['MIT'] 24 | gem.files = `git ls-files`.split($INPUT_RECORD_SEPARATOR) 25 | gem.executables = gem.files.grep(%r{^bin/}).map { |f| File.basename(f) } 26 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 27 | gem.require_paths = ['lib'] 28 | 29 | gem.add_dependency 'mongoid', '>=7.0', '<9.0.0' 30 | gem.add_development_dependency 'rubocop', '~> 0.92' 31 | gem.add_development_dependency 'rubocop-rspec', '~> 1.43' 32 | end 33 | -------------------------------------------------------------------------------- /spec/lib/mongoid/embedded_errors_spec.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.describe Mongoid::EmbeddedErrors do 4 | describe '#errors_with_embedded_errors' do 5 | subject(:article) { Article.new name: 'Test', summary: '-', pages: pages } 6 | 7 | before { |spec| article.validate unless spec.metadata[:do_not_validate] } 8 | 9 | context 'when module is already included in a class', :do_not_validate do 10 | let(:dummy_class) do 11 | Class.new do 12 | include Mongoid::Document 13 | include Mongoid::EmbeddedErrors 14 | end 15 | end 16 | 17 | it 'does not create errors_without_embedded_errors alias again' do 18 | a = dummy_class.instance_method(:errors_without_embedded_errors) 19 | dummy_class.include described_class 20 | 21 | expect( 22 | a 23 | ).to eq(dummy_class.instance_method(:errors_without_embedded_errors)) 24 | end 25 | end 26 | 27 | context 'when article does not have any pages associated' do 28 | let(:pages) { [] } 29 | 30 | it { is_expected.not_to be_valid } 31 | 32 | it "returns `can't be blank` error for pages" do 33 | expect(article.errors[:pages]).to include "can't be blank" 34 | end 35 | end 36 | 37 | context 'when article has one or more invalid pages' do 38 | let(:pages) { [Page.new] } 39 | 40 | it { is_expected.not_to be_valid } 41 | 42 | it 'does not have any errors under `:pages` key' do 43 | expect(article.errors[:pages]).to be_empty 44 | end 45 | 46 | it 'returns all errors for `pages[0]` object' do 47 | expect(article.errors[:'pages[0].title']).to include "can't be blank" 48 | end 49 | end 50 | 51 | context 'when validated multiple times' do 52 | let(:pages) { [Page.new] } 53 | 54 | it 'does not have duplicated errors for the same object' do 55 | article.valid? 56 | expect(article.errors[:'pages[0].title']).to eq(["can't be blank"]) 57 | end 58 | end 59 | 60 | context 'when embeds_many relation is invalid' do 61 | let(:pages) { [Page.new(title: 'Test page', sections: sections)] } 62 | let(:sections) { [Section.new] } 63 | 64 | it 'returns all errors for `sections[0]` object' do 65 | expect(article.errors[:'pages[0].sections[0].header']).to include "can't be blank" 66 | end 67 | end 68 | 69 | context 'when embeds_one relation is invalid' do 70 | subject(:article) { Article.new name: 'Test', summary: '-', pages: pages, annotation: annotation } 71 | 72 | let(:pages) { [Page.new(title: 'Test page')] } 73 | let(:annotation) { Annotation.new } 74 | 75 | it 'returns all errors for `annotation` object' do 76 | expect(article.errors[:'annotation.text']).to include "can't be blank" 77 | end 78 | end 79 | 80 | context 'when embedded document has not been validated', 81 | :do_not_validate do 82 | let(:pages) { [Page.new] } 83 | 84 | it 'does not trigger validations' do 85 | expect(article.errors).to be_empty 86 | end 87 | end 88 | end 89 | end 90 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | require 'bundler/setup' 4 | require 'mongoid-embedded-errors' 5 | require 'database_cleaner-mongoid' 6 | 7 | current_path = File.dirname(__FILE__) 8 | SPEC_MODELS_PATH = File.join(current_path, 'support/**/*.rb').freeze 9 | Dir[SPEC_MODELS_PATH].each { |f| require f } 10 | 11 | Mongoid.load! File.join(current_path, 'support/mongoid.yml'), :test 12 | 13 | RSpec.configure do |config| 14 | config.run_all_when_everything_filtered = true 15 | config.filter_run :focus 16 | config.filter_run_excluding :skip 17 | 18 | config.expect_with :rspec do |expectations| 19 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 20 | expectations.syntax = :expect 21 | end 22 | 23 | config.mock_with :rspec do |mocks| 24 | mocks.verify_partial_doubles = true 25 | end 26 | config.disable_monkey_patching! 27 | 28 | config.before do 29 | # Need to manually reload spec models for mutant to work as expected 30 | if ENV['MUTANT'] 31 | Dir[SPEC_MODELS_PATH].each do |filename| 32 | Object.send(:remove_const, File.basename(filename, '.rb').capitalize) 33 | load filename 34 | end 35 | end 36 | end 37 | end 38 | -------------------------------------------------------------------------------- /spec/support/database_cleaner.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | RSpec.configure do |config| 4 | DatabaseCleaner.strategy = :deletion 5 | config.around do |example| 6 | DatabaseCleaner.cleaning do 7 | example.run 8 | end 9 | end 10 | 11 | config.before(:all) do 12 | DatabaseCleaner.start 13 | end 14 | 15 | config.after(:all) do 16 | DatabaseCleaner.clean 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /spec/support/models/annotation.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Annotation 4 | include Mongoid::Document 5 | 6 | embedded_in :article, inverse_of: :annotation 7 | 8 | field :text, type: String 9 | 10 | validates :text, presence: true 11 | end 12 | -------------------------------------------------------------------------------- /spec/support/models/article.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Article 4 | include Mongoid::Document 5 | include Mongoid::Timestamps 6 | include Mongoid::EmbeddedErrors 7 | 8 | embeds_many :pages 9 | embeds_one :annotation 10 | 11 | field :name, type: String 12 | field :summary, type: String 13 | 14 | validates :name, presence: true 15 | validates :summary, presence: true 16 | validates :pages, presence: true 17 | end 18 | -------------------------------------------------------------------------------- /spec/support/models/page.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Page 4 | include Mongoid::Document 5 | include Mongoid::Timestamps 6 | 7 | embedded_in :article, inverse_of: :pages 8 | embeds_many :sections 9 | 10 | field :title, type: String 11 | 12 | validates :title, presence: true 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/models/section.rb: -------------------------------------------------------------------------------- 1 | # frozen_string_literal: true 2 | 3 | class Section 4 | include Mongoid::Document 5 | include Mongoid::Timestamps 6 | 7 | embedded_in :page, inverse_of: :sections 8 | 9 | field :header, type: String 10 | field :body, type: String 11 | 12 | validates :header, presence: true 13 | end 14 | -------------------------------------------------------------------------------- /spec/support/mongoid.yml: -------------------------------------------------------------------------------- 1 | test: 2 | # mongoid 4 3 | sessions: 4 | default: 5 | database: mongoid_embedded_errors_test 6 | hosts: 7 | - localhost:27017 8 | # mongoid 5 and newer 9 | clients: 10 | default: 11 | database: mongoid_embedded_errors_test 12 | hosts: 13 | - localhost:27017 14 | --------------------------------------------------------------------------------