encore 0.0.3 → 0.1

Sign up to get free protection for your applications and to get access to all the features.
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