encore 0.0.3 → 0.1

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.
Files changed (73) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +0 -16
  3. data/.rubocop.yml +53 -0
  4. data/.travis.yml +10 -2
  5. data/LICENSE.md +1 -1
  6. data/README.md +105 -153
  7. data/Rakefile +10 -0
  8. data/encore.gemspec +16 -13
  9. data/gemfiles/{Gemfile.activerecord-3.2.x → Gemfile.activerecord-4.1} +1 -1
  10. data/lib/encore/config.rb +2 -0
  11. data/lib/encore/persister/errors_parser.rb +17 -0
  12. data/lib/encore/persister/instance.rb +85 -0
  13. data/lib/encore/persister/key_mapping.rb +15 -0
  14. data/lib/encore/persister/links_parser.rb +57 -0
  15. data/lib/encore/persister/param_injection.rb +15 -0
  16. data/lib/encore/serializer/base.rb +48 -0
  17. data/lib/encore/serializer/eager_loading_manager.rb +11 -0
  18. data/lib/encore/serializer/instance.rb +56 -0
  19. data/lib/encore/serializer/linked_resource_manager.rb +19 -0
  20. data/lib/encore/serializer/links_reflection_includer.rb +50 -0
  21. data/lib/encore/serializer/main_resource_links_manager/reflection_belongs_to.rb +13 -0
  22. data/lib/encore/serializer/main_resource_links_manager/reflection_has_many.rb +13 -0
  23. data/lib/encore/serializer/main_resource_links_manager/reflection_has_one.rb +13 -0
  24. data/lib/encore/serializer/main_resource_links_manager.rb +45 -0
  25. data/lib/encore/serializer/main_resource_manager.rb +13 -0
  26. data/lib/encore/serializer/meta_manager.rb +29 -0
  27. data/lib/encore/serializer/options_parser.rb +51 -0
  28. data/lib/encore/serializer/utils.rb +12 -0
  29. data/lib/encore/version.rb +1 -1
  30. data/lib/encore.rb +16 -8
  31. data/spec/encore/persister/belongs_to_links_spec.rb +46 -0
  32. data/spec/encore/persister/create_resources_spec.rb +55 -0
  33. data/spec/encore/persister/errors_resources_spec.rb +55 -0
  34. data/spec/encore/persister/has_many_links_spec.rb +49 -0
  35. data/spec/encore/persister/has_one_links_spec.rb +49 -0
  36. data/spec/encore/persister/key_mappings_spec.rb +66 -0
  37. data/spec/encore/persister/param_injection_spec.rb +46 -0
  38. data/spec/encore/persister/update_resources_spec.rb +37 -0
  39. data/spec/encore/serializer/linked/always_include_resources_spec.rb +85 -0
  40. data/spec/encore/serializer/linked/can_include_resources_spec.rb +69 -0
  41. data/spec/encore/serializer/linked/linked_resources_spec.rb +103 -0
  42. data/spec/encore/serializer/links_resource_spec.rb +294 -0
  43. data/spec/encore/serializer/main_meta_spec.rb +66 -0
  44. data/spec/encore/serializer/main_resource_spec.rb +37 -0
  45. data/spec/encore/serializer/no_paging_spec.rb +40 -0
  46. data/spec/encore_spec.rb +4 -0
  47. data/spec/spec_helper.rb +8 -21
  48. data/spec/support/macros/database/database_adapter.rb +9 -0
  49. data/spec/support/macros/database/sqlite3_adapter.rb +14 -0
  50. data/spec/support/macros/database_macros.rb +5 -16
  51. data/spec/support/macros/model_macros.rb +7 -22
  52. metadata +143 -72
  53. data/lib/encore/association/has_many_association.rb +0 -12
  54. data/lib/encore/association/has_one_association.rb +0 -12
  55. data/lib/encore/association.rb +0 -39
  56. data/lib/encore/attribute.rb +0 -42
  57. data/lib/encore/entity/input/associations.rb +0 -27
  58. data/lib/encore/entity/input/errors.rb +0 -8
  59. data/lib/encore/entity/input/exposed_attributes.rb +0 -37
  60. data/lib/encore/entity/input.rb +0 -73
  61. data/lib/encore/entity/output/json.rb +0 -14
  62. data/lib/encore/entity/output.rb +0 -13
  63. data/lib/encore/entity.rb +0 -44
  64. data/lib/encore/helpers/controller_helper.rb +0 -37
  65. data/lib/encore/helpers.rb +0 -1
  66. data/lib/encore/railtie.rb +0 -11
  67. data/spec/encore/entity/input/associations_spec.rb +0 -48
  68. data/spec/encore/entity/input/exposed_attributes_spec.rb +0 -46
  69. data/spec/encore/entity/input_spec.rb +0 -104
  70. data/spec/encore/entity/output/json_spec.rb +0 -76
  71. data/spec/encore/entity/output_spec.rb +0 -5
  72. data/spec/encore/entity_spec.rb +0 -31
  73. data/spec/encore/helpers/controller_helper_spec.rb +0 -59
@@ -0,0 +1,57 @@
1
+ module Encore
2
+ module Persister
3
+ module LinksParser
4
+ extend ActiveSupport::Concern
5
+
6
+ def parse_links(args)
7
+ links = args.delete(:links) || []
8
+
9
+ links.each do |link, value|
10
+ reflection = @model.reflections[link.to_sym]
11
+ key = fetch_key(reflection)
12
+ value = fetch_value(value, reflection)
13
+
14
+ args.merge!(key => value) if key
15
+ end
16
+
17
+ args
18
+ end
19
+
20
+ private
21
+
22
+ # `belongs_to` => 'user' become 'user_id'
23
+ def link_belongs_to(reflection)
24
+ reflection.foreign_key.to_sym
25
+ end
26
+
27
+ # `has_many` => 'users' become 'user_ids'
28
+ def link_has_many(reflection)
29
+ "#{reflection.name.to_s.singularize}_ids".to_sym
30
+ end
31
+
32
+ # `has_one` => 'user' stay 'user'
33
+ def link_has_one(reflection)
34
+ reflection.name
35
+ end
36
+
37
+ # Convert reflection with the right ActiveRecord key for insert/update.
38
+ def fetch_key(reflection)
39
+ case reflection.macro
40
+ when :belongs_to then link_belongs_to(reflection)
41
+ when :has_many then link_has_many(reflection)
42
+ when :has_one then link_has_one(reflection)
43
+ end
44
+ end
45
+
46
+ # Value for `has_one` must be an activerecord instance.
47
+ # Other reflection macro types stay the same.
48
+ def fetch_value(value, reflection)
49
+ if reflection.macro == :has_one
50
+ reflection.klass.find(value)
51
+ else
52
+ value
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,15 @@
1
+ module Encore
2
+ module Persister
3
+ module ParamInjection
4
+ def self.inject(payload, injection)
5
+ return payload if injection.nil?
6
+
7
+ payload.map do |param|
8
+ injection.reduce(param) do |memo, (key, value)|
9
+ memo.merge key => value
10
+ end
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,48 @@
1
+ require 'encore/serializer/links_reflection_includer'
2
+
3
+ module Encore
4
+ module Serializer
5
+ class Base < ::ActiveModel::Serializer
6
+ def id
7
+ object.id.to_s
8
+ end
9
+
10
+ def links
11
+ object.reflections.each_with_object({}) do |(_, reflection), memo|
12
+ if object.association(reflection.name).loaded?
13
+ fetcher = LinksReflectionIncluder::Loaded
14
+ else
15
+ next unless self.class.can_access.include?(reflection.name)
16
+ fetcher = LinksReflectionIncluder::NotLoaded
17
+ end
18
+
19
+ memo.merge!(reflection.name => fetcher.send("reflection_#{reflection.macro}", object, reflection))
20
+ end
21
+ end
22
+
23
+ # Specify which resources the API can be included in the "linked" top-level key.
24
+ def self.can_include
25
+ []
26
+ end
27
+
28
+ # Specify which resources the API always include in the "linked" top-level key.
29
+ def self.always_include
30
+ []
31
+ end
32
+
33
+ # Specify which resources the API exposes URL to.
34
+ # Default is can_include + always_include.
35
+ def self.can_access
36
+ can_include | always_include
37
+ end
38
+
39
+ def self.root_key
40
+ model_class.name.pluralize.underscore.to_sym
41
+ end
42
+
43
+ def self.key_mappings
44
+ {}
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,11 @@
1
+ module Encore
2
+ module Serializer
3
+ module EagerLoadingManager
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.add(collection, option_include)
7
+ collection.includes(option_include)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,56 @@
1
+ require 'encore/serializer/eager_loading_manager'
2
+ require 'encore/serializer/linked_resource_manager'
3
+ require 'encore/serializer/main_resource_manager'
4
+ require 'encore/serializer/main_resource_links_manager'
5
+ require 'encore/serializer/meta_manager'
6
+ require 'encore/serializer/utils'
7
+ require 'encore/serializer/options_parser'
8
+
9
+ module Encore
10
+ module Serializer
11
+ class Instance
12
+ def initialize(collection, opts = {})
13
+ @collection = collection
14
+ @serializers = [serializer]
15
+ @options = parsed_options(opts)
16
+ end
17
+
18
+ def as_json(*_)
19
+ # Prepare main collection
20
+ @collection = MetaManager.paginate_collection(@collection, @options)
21
+ @collection = EagerLoadingManager.add(@collection, @options[:include])
22
+
23
+ # Fetch linked ids
24
+ linked_ids = MainResourceLinksManager.add(@collection, reflections, @options[:include])
25
+
26
+ # Build final output
27
+ output = MainResourceManager.add(@collection, serializer)
28
+ output.merge! linked: LinkedResourceManager.add(linked_ids)
29
+ output.merge! meta: MetaManager.add(@collection, serializer, @options)
30
+
31
+ output
32
+ end
33
+
34
+ private
35
+
36
+ def reflections
37
+ @reflections ||= @collection.klass.reflections
38
+ end
39
+
40
+ def serializer
41
+ @serializer ||= Utils.fetch_serializer(@collection.klass)
42
+ end
43
+
44
+ def parsed_options(opts)
45
+ parser = OptionsParser.new(opts)
46
+
47
+ {
48
+ include: parser.include(serializer),
49
+ skip_paging: parser.skip_paging,
50
+ page: parser.page,
51
+ per_page: parser.per_page
52
+ }
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,19 @@
1
+ require 'encore/serializer/utils'
2
+
3
+ module Encore
4
+ module Serializer
5
+ module LinkedResourceManager
6
+ extend ActiveSupport::Concern
7
+
8
+ def self.add(linked_ids)
9
+ linked_ids.reduce({}) do |memo, (model, ids)|
10
+ klass = model.constantize
11
+ serializer = Utils.fetch_serializer(klass)
12
+
13
+ collection = klass.where(id: ids.to_a)
14
+ memo.merge! MainResourceManager.add(collection, serializer)
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,50 @@
1
+ module Encore
2
+ module Serializer
3
+ module LinksReflectionIncluder
4
+ module Loaded
5
+ def self.reflection_belongs_to(object, reflection)
6
+ object.send(reflection.foreign_key).try(:to_s) if object.respond_to?(reflection.foreign_key)
7
+ end
8
+
9
+ def self.reflection_has_one(object, reflection)
10
+ object.send(reflection.name).try(:id).try(:to_s)
11
+ end
12
+
13
+ def self.reflection_has_many(object, reflection)
14
+ object.send("#{reflection.name.to_s.singularize}_ids").map(&:to_s) if object.send(reflection.name).loaded?
15
+ end
16
+ end
17
+
18
+ module NotLoaded
19
+ def self.reflection_has_many(object, reflection)
20
+ reflection_type = reflection.name.to_s.pluralize
21
+
22
+ {
23
+ href: "/#{reflection_type}?#{object.class.name.downcase}_id=#{object.id}",
24
+ type: reflection_type
25
+ }
26
+ end
27
+
28
+ def self.reflection_has_one(object, reflection)
29
+ reflection_type = reflection.name.to_s
30
+
31
+ {
32
+ href: "/#{object.class.name.downcase.pluralize}/#{object.id}/#{reflection_type}",
33
+ type: reflection_type.pluralize
34
+ }
35
+ end
36
+
37
+ def self.reflection_belongs_to(object, reflection)
38
+ reflection_type = reflection.name.to_s.pluralize
39
+ reflection_id = object.send(reflection.foreign_key).try(:to_s)
40
+
41
+ {
42
+ href: "/#{reflection_type}/#{reflection_id}",
43
+ id: reflection_id,
44
+ type: reflection_type
45
+ }
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,13 @@
1
+ module Encore
2
+ module Serializer
3
+ module MainResourceLinksManager
4
+ module ReflectionBelongsTo
5
+ extend ActiveSupport::Concern
6
+
7
+ def self.add(item, reflection)
8
+ [item.send(reflection.foreign_key).try(:to_s)]
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Encore
2
+ module Serializer
3
+ module MainResourceLinksManager
4
+ module ReflectionHasMany
5
+ extend ActiveSupport::Concern
6
+
7
+ def self.add(item, reflection)
8
+ item.send("#{reflection.name.to_s.singularize}_ids").map(&:to_s)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ module Encore
2
+ module Serializer
3
+ module MainResourceLinksManager
4
+ module ReflectionHasOne
5
+ extend ActiveSupport::Concern
6
+
7
+ def self.add(item, reflection)
8
+ [item.send(reflection.name).try(:id).try(:to_s)]
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,45 @@
1
+ require 'encore/serializer/main_resource_links_manager/reflection_belongs_to'
2
+ require 'encore/serializer/main_resource_links_manager/reflection_has_many'
3
+ require 'encore/serializer/main_resource_links_manager/reflection_has_one'
4
+
5
+ module Encore
6
+ module Serializer
7
+ module MainResourceLinksManager
8
+ extend ActiveSupport::Concern
9
+
10
+ def self.add(collection, reflections, option_include)
11
+ collection.each_with_object(Hash.new(Set.new)) do |resource, memo|
12
+ option_include.each do |inclusion|
13
+ model, ids = association_collection(resource, inclusion, reflections)
14
+ memo[model] += ids
15
+ end
16
+ end
17
+ end
18
+
19
+ private
20
+
21
+ def self.association_collection(item, inclusion, reflections)
22
+ reflection = reflections[inclusion]
23
+ class_name = fetch_class_name(item, reflection)
24
+
25
+ collection = begin
26
+ case reflection.macro
27
+ when :belongs_to then ReflectionBelongsTo.add(item, reflection)
28
+ when :has_one then ReflectionHasOne.add(item, reflection)
29
+ when :has_many then ReflectionHasMany.add(item, reflection)
30
+ end
31
+ end
32
+
33
+ [class_name, collection.flatten.compact]
34
+ end
35
+
36
+ def self.fetch_class_name(item, reflection)
37
+ if reflection.options[:polymorphic]
38
+ item.send(reflection.foreign_type).constantize.to_s
39
+ else
40
+ reflection.klass.name.to_s
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,13 @@
1
+ module Encore
2
+ module Serializer
3
+ module MainResourceManager
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.add(collection, serializer)
7
+ {
8
+ serializer.root_key => collection.map { |o| serializer.new(o).as_json }
9
+ }
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,29 @@
1
+ module Encore
2
+ module Serializer
3
+ module MetaManager
4
+ extend ActiveSupport::Concern
5
+
6
+ def self.paginate_collection(collection, options)
7
+ return collection if options[:skip_paging]
8
+
9
+ collection.page(options[:page]).per(options[:per_page])
10
+ end
11
+
12
+ def self.add(collection, serializer, options)
13
+ return {} if options[:skip_paging]
14
+
15
+ { serializer.root_key => pagination_for(collection) }
16
+ end
17
+
18
+ def self.pagination_for(collection)
19
+ {
20
+ page: collection.current_page,
21
+ count: collection.total_count,
22
+ page_count: collection.num_pages,
23
+ previous_page: collection.prev_page,
24
+ next_page: collection.next_page
25
+ }
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,51 @@
1
+ module Encore
2
+ module Serializer
3
+ class OptionsParser
4
+ def initialize(opts)
5
+ @opts = opts
6
+ end
7
+
8
+ def include(serializer)
9
+ opt = @opts[:include]
10
+
11
+ output = []
12
+
13
+ # Split includes list
14
+ output += opt.split(',').map(&:to_sym) if opt.present?
15
+
16
+ # Remove resource type not included in 'can_include'
17
+ output = serializer.can_include & output if serializer.respond_to?(:can_include)
18
+
19
+ # Add 'always_include' resource type
20
+ output += serializer.always_include if serializer.respond_to?(:always_include)
21
+
22
+ output
23
+ end
24
+
25
+ def page
26
+ opt = @opts[:page]
27
+
28
+ opt.present? ? opt.to_i : 1
29
+ end
30
+
31
+ def per_page
32
+ opt = @opts[:per_page]
33
+
34
+ return Kaminari.config.default_per_page if opt.nil?
35
+
36
+ max_per_page = Kaminari.config.max_per_page.to_i
37
+ opt = opt.to_i
38
+
39
+ if max_per_page.zero?
40
+ opt
41
+ else
42
+ opt > max_per_page ? max_per_page : opt
43
+ end
44
+ end
45
+
46
+ def skip_paging
47
+ @opts[:skip_paging].present?
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,12 @@
1
+ module Encore
2
+ module Serializer
3
+ module Utils
4
+ def self.fetch_serializer(model)
5
+ default_serializer = (model.name.gsub('::', '') + 'Serializer')
6
+ model.active_model_serializer || default_serializer.constantize
7
+ rescue NameError
8
+ raise NameError, "can’t find serializer for #{model.name}, try creating #{default_serializer}"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -1,3 +1,3 @@
1
1
  module Encore
2
- VERSION = '0.0.3'
2
+ VERSION = '0.1'
3
3
  end
data/lib/encore.rb CHANGED
@@ -1,13 +1,21 @@
1
1
  require 'encore/version'
2
2
 
3
- # ActiveSupport
3
+ require 'active_model_serializers'
4
+ require 'active_record'
4
5
  require 'active_support'
5
6
 
6
- # Encore
7
- require 'encore/entity'
8
- require 'encore/attribute'
9
- require 'encore/association'
10
- require 'encore/helpers'
7
+ # This is a dirty hack to fix dirty code
8
+ # https://github.com/amatsuda/kaminari/blob/7b049067b143212a172d5bb472184eac23121f34/lib/kaminari.rb#L11-L25
9
+ oldstderr = $stderr.dup
10
+ $stderr = StringIO.new
11
+ require 'kaminari'
12
+ $stderr = oldstderr
11
13
 
12
- # Railtie
13
- require 'encore/railtie' if defined?(Rails) && Rails::VERSION::MAJOR >= 3
14
+ require 'encore/config'
15
+
16
+ require 'encore/serializer/base'
17
+ require 'encore/serializer/instance'
18
+ require 'encore/persister/instance'
19
+
20
+ module Encore
21
+ end
@@ -0,0 +1,46 @@
1
+ require 'spec_helper'
2
+
3
+ describe Encore::Persister do
4
+ let(:persister) { Encore::Persister::Instance }
5
+ let(:persist!) { persister.new(model, params).persist! }
6
+
7
+ before do
8
+ run_migrations!
9
+ spawn_models!
10
+ end
11
+
12
+ let(:run_migrations!) do
13
+ run_migration do
14
+ create_table(:users, force: true) do |t|
15
+ t.string :name, default: nil
16
+ t.integer :project_id, default: nil
17
+ end
18
+ create_table(:projects, force: true) do |t|
19
+ t.string :name, default: nil
20
+ end
21
+ end
22
+ end
23
+
24
+ let(:spawn_models!) do
25
+ spawn_model('User') do
26
+ belongs_to :project
27
+ end
28
+ spawn_model('Project')
29
+ end
30
+
31
+ let(:model) { User }
32
+ let(:project1) { Project.create name: 'a' }
33
+
34
+ let(:params) do
35
+ [{
36
+ name: 'Allan',
37
+ links: {
38
+ project: project1.id.to_s
39
+ }
40
+ }]
41
+ end
42
+
43
+ it { expect { persist! }.to change { model.count }.by(1) }
44
+ it { expect { persist! }.to change { model.first.try(:name) }.to('Allan') }
45
+ it { expect { persist! }.to change { model.first.try(:project) }.to(project1) }
46
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Encore::Persister do
4
+ let(:persister) { Encore::Persister::Instance }
5
+ let(:persist!) { persister.new(model, params).persist! }
6
+
7
+ let(:run_migrations!) do
8
+ run_migration do
9
+ create_table(:users, force: true) do |t|
10
+ t.string :name, default: nil
11
+ t.string :phone, default: nil
12
+ end
13
+ end
14
+ end
15
+
16
+ let(:spawn_models!) do
17
+ spawn_model('User')
18
+ end
19
+
20
+ let(:model) { User }
21
+
22
+ before do
23
+ run_migrations!
24
+ spawn_models!
25
+ end
26
+
27
+ context 'single create' do
28
+ let(:params) do
29
+ [{
30
+ name: 'Allan',
31
+ phone: '555-2525'
32
+ }]
33
+ end
34
+
35
+ it { expect { persist! }.to change { model.count }.by(1) }
36
+ it { expect { persist! }.to change { model.first.try(:name) }.to('Allan') }
37
+ it { expect { persist! }.to change { model.first.try(:phone) }.to('555-2525') }
38
+ end
39
+
40
+ context 'many create' do
41
+ let(:params) do
42
+ [{
43
+ name: 'Allan',
44
+ phone: '555-2525'
45
+ }, {
46
+ name: 'Bob'
47
+ }]
48
+ end
49
+
50
+ it { expect { persist! }.to change { model.count }.by(2) }
51
+ it { expect { persist! }.to change { model.first.try(:name) }.to('Allan') }
52
+ it { expect { persist! }.to change { model.first.try(:phone) }.to('555-2525') }
53
+ it { expect { persist! }.to change { model.last.try(:name) }.to('Bob') }
54
+ end
55
+ end
@@ -0,0 +1,55 @@
1
+ require 'spec_helper'
2
+
3
+ describe Encore::Persister do
4
+ let(:persister) { Encore::Persister::Instance.new(model, params) }
5
+ let(:persist!) { persister.persist! }
6
+
7
+ let(:run_migrations!) do
8
+ run_migration do
9
+ create_table(:users, force: true) do |t|
10
+ t.string :name, default: nil
11
+ end
12
+ end
13
+ end
14
+
15
+ let(:spawn_models!) do
16
+ spawn_model('User') do
17
+ validates :name, presence: true, length: { minimum: 2 }
18
+ end
19
+ end
20
+
21
+ before do
22
+ run_migrations!
23
+ spawn_models!
24
+ end
25
+
26
+ let(:params) do
27
+ [{
28
+ name: ''
29
+ }]
30
+ end
31
+
32
+ let(:expected_error) do
33
+ {
34
+ field: 'name',
35
+ types: ['can\'t be blank', 'is too short (minimum is 2 characters)'],
36
+ path: 'user/0/name'
37
+ }
38
+ end
39
+
40
+ context 'create' do
41
+ let(:model) { User }
42
+
43
+ it { expect { persist! }.to_not change { model.count } }
44
+ it { expect { persist! }.to change { persister.errors.count }.to(1) }
45
+ it { expect { persist! }.to change { persister.errors.first }.to(expected_error) }
46
+ end
47
+
48
+ context 'update' do
49
+ let(:model) { User.create name: 'Robert' }
50
+
51
+ it { expect { persist! }.to_not change { model.reload.name } }
52
+ it { expect { persist! }.to change { persister.errors.count }.to(1) }
53
+ it { expect { persist! }.to change { persister.errors.first }.to(expected_error) }
54
+ end
55
+ end