her 0.8.2 → 0.10.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.rspec +1 -1
  3. data/.rubocop.yml +1291 -0
  4. data/.travis.yml +6 -1
  5. data/README.md +29 -11
  6. data/her.gemspec +3 -5
  7. data/lib/her/api.rb +16 -9
  8. data/lib/her/middleware/json_api_parser.rb +1 -1
  9. data/lib/her/model/associations/association.rb +32 -5
  10. data/lib/her/model/associations/association_proxy.rb +1 -1
  11. data/lib/her/model/associations/belongs_to_association.rb +1 -1
  12. data/lib/her/model/associations/has_many_association.rb +3 -3
  13. data/lib/her/model/attributes.rb +105 -75
  14. data/lib/her/model/http.rb +3 -3
  15. data/lib/her/model/introspection.rb +1 -1
  16. data/lib/her/model/orm.rb +96 -19
  17. data/lib/her/model/parse.rb +27 -17
  18. data/lib/her/model/relation.rb +46 -2
  19. data/lib/her/version.rb +1 -1
  20. data/spec/api_spec.rb +34 -31
  21. data/spec/collection_spec.rb +25 -10
  22. data/spec/json_api/model_spec.rb +75 -72
  23. data/spec/middleware/accept_json_spec.rb +1 -1
  24. data/spec/middleware/first_level_parse_json_spec.rb +20 -20
  25. data/spec/middleware/json_api_parser_spec.rb +26 -7
  26. data/spec/middleware/second_level_parse_json_spec.rb +8 -9
  27. data/spec/model/associations/association_proxy_spec.rb +2 -5
  28. data/spec/model/associations_spec.rb +617 -295
  29. data/spec/model/attributes_spec.rb +114 -107
  30. data/spec/model/callbacks_spec.rb +59 -27
  31. data/spec/model/dirty_spec.rb +70 -29
  32. data/spec/model/http_spec.rb +67 -35
  33. data/spec/model/introspection_spec.rb +26 -22
  34. data/spec/model/nested_attributes_spec.rb +31 -31
  35. data/spec/model/orm_spec.rb +332 -157
  36. data/spec/model/parse_spec.rb +250 -77
  37. data/spec/model/paths_spec.rb +109 -109
  38. data/spec/model/relation_spec.rb +89 -69
  39. data/spec/model/validations_spec.rb +6 -6
  40. data/spec/model_spec.rb +17 -17
  41. data/spec/spec_helper.rb +2 -3
  42. data/spec/support/macros/model_macros.rb +2 -2
  43. metadata +36 -63
data/lib/her/model/orm.rb CHANGED
@@ -39,17 +39,13 @@ module Her
39
39
  callback = new? ? :create : :update
40
40
  method = self.class.method_for(callback)
41
41
 
42
- run_callbacks callback do
43
- run_callbacks :save do
44
- params = to_params
42
+ run_callbacks :save do
43
+ run_callbacks callback do
45
44
  self.class.request(to_params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
46
- assign_attributes(self.class.parse(parsed_data[:data])) if parsed_data[:data].any?
47
- @metadata = parsed_data[:metadata]
48
- @response_errors = parsed_data[:errors]
49
-
45
+ load_from_parsed_data(parsed_data)
50
46
  return false if !response.success? || @response_errors.any?
51
47
  if self.changed_attributes.present?
52
- @previously_changed = self.changed_attributes.clone
48
+ @previously_changed = self.changes.clone
53
49
  self.changed_attributes.clear
54
50
  end
55
51
  end
@@ -77,15 +73,92 @@ module Her
77
73
  method = self.class.method_for(:destroy)
78
74
  run_callbacks :destroy do
79
75
  self.class.request(params.merge(:_method => method, :_path => request_path)) do |parsed_data, response|
80
- assign_attributes(self.class.parse(parsed_data[:data])) if parsed_data[:data].any?
81
- @metadata = parsed_data[:metadata]
82
- @response_errors = parsed_data[:errors]
83
- @destroyed = true
76
+ load_from_parsed_data(parsed_data)
77
+ @destroyed = response.success?
84
78
  end
85
79
  end
86
80
  self
87
81
  end
88
82
 
83
+ # Initializes +attribute+ to zero if +nil+ and adds the value passed as
84
+ # +by+ (default is 1). The increment is performed directly on the
85
+ # underlying attribute, no setter is invoked. Only makes sense for
86
+ # number-based attributes. Returns +self+.
87
+ def increment(attribute, by = 1)
88
+ attributes[attribute] ||= 0
89
+ attributes[attribute] += by
90
+ self
91
+ end
92
+
93
+ # Wrapper around #increment that saves the resource. Saving is subjected
94
+ # to validation checks. Returns +self+.
95
+ def increment!(attribute, by = 1)
96
+ increment(attribute, by) && save
97
+ self
98
+ end
99
+
100
+ # Initializes +attribute+ to zero if +nil+ and substracts the value passed as
101
+ # +by+ (default is 1). The decrement is performed directly on the
102
+ # underlying attribute, no setter is invoked. Only makes sense for
103
+ # number-based attributes. Returns +self+.
104
+ def decrement(attribute, by = 1)
105
+ increment(attribute, -by)
106
+ end
107
+
108
+ # Wrapper around #decrement that saves the resource. Saving is subjected
109
+ # to validation checks. Returns +self+.
110
+ def decrement!(attribute, by = 1)
111
+ increment!(attribute, -by)
112
+ end
113
+
114
+ # Assigns to +attribute+ the boolean opposite of <tt>attribute?</tt>. So
115
+ # if the predicate returns +true+ the attribute will become +false+. This
116
+ # method toggles directly the underlying value without calling any setter.
117
+ # Returns +self+.
118
+ #
119
+ # @example
120
+ # user = User.first
121
+ # user.admin? # => false
122
+ # user.toggle(:admin)
123
+ # user.admin? # => true
124
+ def toggle(attribute)
125
+ attributes[attribute] = !public_send("#{attribute}?")
126
+ self
127
+ end
128
+
129
+ # Wrapper around #toggle that saves the resource. Saving is subjected to
130
+ # validation checks. Returns +true+ if the record could be saved.
131
+ def toggle!(attribute)
132
+ toggle(attribute) && save
133
+ end
134
+
135
+ # Refetches the resource
136
+ #
137
+ # This method finds the resource by its primary key (which could be
138
+ # assigned manually) and modifies the object in-place.
139
+ #
140
+ # @example
141
+ # user = User.find(1)
142
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
143
+ # user.name = "Oops"
144
+ # user.reload # Fetched again via GET "/users/1"
145
+ # # => #<User(users/1) id=1 name="Tobias Fünke">
146
+ def reload(options = nil)
147
+ fresh_object = self.class.find(id)
148
+ assign_attributes(fresh_object.attributes)
149
+ self
150
+ end
151
+
152
+ # Uses parsed response to assign attributes and metadata
153
+ #
154
+ # @private
155
+ def load_from_parsed_data(parsed_data)
156
+ data = parsed_data[:data]
157
+ assign_attributes(self.class.parse(data)) if data.any?
158
+ @metadata = parsed_data[:metadata]
159
+ @response_errors = parsed_data[:errors]
160
+ end
161
+
89
162
  module ClassMethods
90
163
  # Create a new chainable scope
91
164
  #
@@ -105,10 +178,8 @@ module Her
105
178
  instance_exec(*args, &code)
106
179
  end
107
180
 
108
- # Add the scope method to the Relation class
109
- Relation.instance_eval do
110
- define_method(name) { |*args| instance_exec(*args, &code) }
111
- end
181
+ # Add the scope method to the default/blank relation
182
+ scoped.define_singleton_method(name) { |*args| instance_exec(*args, &code) }
112
183
  end
113
184
 
114
185
  # @private
@@ -134,7 +205,8 @@ module Her
134
205
  end
135
206
 
136
207
  # Delegate the following methods to `scoped`
137
- [:all, :where, :create, :build, :find, :first_or_create, :first_or_initialize].each do |method|
208
+ [:all, :where, :create, :build, :find, :find_by, :find_or_create_by,
209
+ :find_or_initialize_by, :first_or_create, :first_or_initialize].each do |method|
138
210
  class_eval <<-RUBY, __FILE__, __LINE__ + 1
139
211
  def #{method}(*params)
140
212
  scoped.send(#{method.to_sym.inspect}, *params)
@@ -160,7 +232,12 @@ module Her
160
232
  # # Called via DELETE "/users/1"
161
233
  def destroy_existing(id, params={})
162
234
  request(params.merge(:_method => method_for(:destroy), :_path => build_request_path(params.merge(primary_key => id)))) do |parsed_data, response|
163
- new(parse(parsed_data[:data]).merge(:_destroyed => true))
235
+ data = parse(parsed_data[:data])
236
+ metadata = parsed_data[:metadata]
237
+ response_errors = parsed_data[:errors]
238
+ record = new(data.merge(:_destroyed => response.success?, :metadata => metadata))
239
+ record.response_errors = response_errors
240
+ record
164
241
  end
165
242
  end
166
243
 
@@ -196,9 +273,9 @@ module Her
196
273
  resource
197
274
  end
198
275
 
199
- private
200
276
  # @private
201
277
  def blank_relation
278
+ @blank_relation ||= superclass.blank_relation.clone.tap { |r| r.parent = self } if superclass.respond_to?(:blank_relation)
202
279
  @blank_relation ||= Relation.new(self)
203
280
  end
204
281
  end
@@ -32,8 +32,18 @@ module Her
32
32
 
33
33
  # @private
34
34
  def to_params(attributes, changes={})
35
- filtered_attributes = attributes.dup.symbolize_keys
35
+ filtered_attributes = attributes.each_with_object({}) do |(key, value), memo|
36
+ case value
37
+ when Her::Model
38
+ when ActiveModel::Serialization
39
+ value = value.serializable_hash.symbolize_keys
40
+ end
41
+
42
+ memo[key.to_sym] = value
43
+ end
44
+
36
45
  filtered_attributes.merge!(embeded_params(attributes))
46
+
37
47
  if her_api.options[:send_only_modified_attributes]
38
48
  filtered_attributes = changes.symbolize_keys.keys.inject({}) do |hash, attribute|
39
49
  hash[attribute] = filtered_attributes[attribute]
@@ -52,20 +62,16 @@ module Her
52
62
  end
53
63
  end
54
64
 
55
-
56
65
  # @private
57
- # TODO: Handle has_one
58
66
  def embeded_params(attributes)
59
- associations[:has_many].select { |a| attributes.include?(a[:data_key])}.compact.inject({}) do |hash, association|
60
- params = attributes[association[:data_key]].map(&:to_params)
61
- next if params.empty?
62
- if association[:class_name].constantize.include_root_in_json?
63
- root = association[:class_name].constantize.root_element
64
- hash[association[:data_key]] = params.map { |n| n[root] }
65
- else
66
- hash[association[:data_key]] = params
67
- end
68
- hash
67
+ associations.values.flatten.each_with_object({}) do |definition, hash|
68
+ value = case association = attributes[definition[:name]]
69
+ when Her::Collection, Array
70
+ association.map { |a| a.to_params }.reject(&:empty?)
71
+ when Her::Model
72
+ association.to_params
73
+ end
74
+ hash[definition[:data_key]] = value if value.present?
69
75
  end
70
76
  end
71
77
 
@@ -139,7 +145,8 @@ module Her
139
145
 
140
146
  # @private
141
147
  def root_element_included?(data)
142
- data.keys.to_s.include? @_her_root_element.to_s
148
+ element = data[parsed_root_element]
149
+ element.is_a?(Hash) || element.is_a?(Array)
143
150
  end
144
151
 
145
152
  # @private
@@ -198,17 +205,20 @@ module Her
198
205
 
199
206
  # @private
200
207
  def request_new_object_on_build?
201
- @_her_request_new_object_on_build || (superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?)
208
+ return @_her_request_new_object_on_build unless @_her_request_new_object_on_build.nil?
209
+ superclass.respond_to?(:request_new_object_on_build?) && superclass.request_new_object_on_build?
202
210
  end
203
211
 
204
212
  # @private
205
213
  def include_root_in_json?
206
- @_her_include_root_in_json || (superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?)
214
+ return @_her_include_root_in_json unless @_her_include_root_in_json.nil?
215
+ superclass.respond_to?(:include_root_in_json?) && superclass.include_root_in_json?
207
216
  end
208
217
 
209
218
  # @private
210
219
  def parse_root_in_json?
211
- @_her_parse_root_in_json || (superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?)
220
+ return @_her_parse_root_in_json unless @_her_parse_root_in_json.nil?
221
+ superclass.respond_to?(:parse_root_in_json?) && superclass.parse_root_in_json?
212
222
  end
213
223
  end
214
224
  end
@@ -3,6 +3,7 @@ module Her
3
3
  class Relation
4
4
  # @private
5
5
  attr_accessor :params
6
+ attr_writer :parent
6
7
 
7
8
  # @private
8
9
  def initialize(parent)
@@ -65,7 +66,7 @@ module Her
65
66
  # @private
66
67
  def fetch
67
68
  @_fetch ||= begin
68
- path = @parent.build_request_path(@params)
69
+ path = @parent.build_request_path(@parent.collection_path, @params)
69
70
  method = @parent.method_for(:find)
70
71
  @parent.request(@params.merge(:_method => method, :_path => path)) do |parsed_data, response|
71
72
  @parent.new_collection(parsed_data)
@@ -96,7 +97,6 @@ module Her
96
97
  @parent.request(request_params) do |parsed_data, response|
97
98
  if response.success?
98
99
  resource = @parent.new_from_parsed_data(parsed_data)
99
- resource.instance_variable_set(:@changed_attributes, {})
100
100
  resource.run_callbacks :find
101
101
  else
102
102
  return nil
@@ -109,6 +109,50 @@ module Her
109
109
  ids.length > 1 || ids.first.kind_of?(Array) ? results : results.first
110
110
  end
111
111
 
112
+ # Fetch first resource with the given attributes.
113
+ #
114
+ # If no resource is found, returns <tt>nil</tt>.
115
+ #
116
+ # @example
117
+ # @user = User.find_by(name: "Tobias", age: 42)
118
+ # # Called via GET "/users?name=Tobias&age=42"
119
+ def find_by(params)
120
+ where(params).first
121
+ end
122
+
123
+ # Fetch first resource with the given attributes, or create a resource
124
+ # with the attributes if one is not found.
125
+ #
126
+ # @example
127
+ # @user = User.find_or_create_by(email: "remi@example.com")
128
+ #
129
+ # # Returns the first item in the collection if present:
130
+ # # Called via GET "/users?email=remi@example.com"
131
+ #
132
+ # # If collection is empty:
133
+ # # POST /users with `email=remi@example.com`
134
+ # @user.email # => "remi@example.com"
135
+ # @user.new? # => false
136
+ def find_or_create_by(attributes)
137
+ find_by(attributes) || create(attributes)
138
+ end
139
+
140
+ # Fetch first resource with the given attributes, or initialize a resource
141
+ # with the attributes if one is not found.
142
+ #
143
+ # @example
144
+ # @user = User.find_or_initialize_by(email: "remi@example.com")
145
+ #
146
+ # # Returns the first item in the collection if present:
147
+ # # Called via GET "/users?email=remi@example.com"
148
+ #
149
+ # # If collection is empty:
150
+ # @user.email # => "remi@example.com"
151
+ # @user.new? # => true
152
+ def find_or_initialize_by(attributes)
153
+ find_by(attributes) || build(attributes)
154
+ end
155
+
112
156
  # Create a resource and return it
113
157
  #
114
158
  # @example
data/lib/her/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Her
2
- VERSION = "0.8.2"
2
+ VERSION = "0.10.4"
3
3
  end
data/spec/api_spec.rb CHANGED
@@ -8,21 +8,24 @@ describe Her::API do
8
8
  describe "#setup" do
9
9
  context "when setting custom middleware" do
10
10
  before do
11
- class Foo; end;
12
- class Bar; end;
11
+ class Foo; end
12
+ class Bar; end
13
13
 
14
- subject.setup :url => "https://api.example.com" do |connection|
14
+ subject.setup url: "https://api.example.com" do |connection|
15
15
  connection.use Foo
16
16
  connection.use Bar
17
17
  end
18
18
  end
19
19
 
20
- specify { subject.connection.builder.handlers.should == [Foo, Bar] }
20
+ specify { expect(subject.connection.builder.handlers).to eq([Foo, Bar]) }
21
21
  end
22
22
 
23
23
  context "when setting custom options" do
24
- before { subject.setup :foo => { :bar => "baz" }, :url => "https://api.example.com" }
25
- its(:options) { should == { :foo => { :bar => "baz" }, :url => "https://api.example.com" } }
24
+ before { subject.setup foo: { bar: "baz" }, url: "https://api.example.com" }
25
+
26
+ describe "#options" do
27
+ it { expect(subject.options).to eq(foo: { bar: "baz" }, url: "https://api.example.com") }
28
+ end
26
29
  end
27
30
  end
28
31
 
@@ -30,83 +33,83 @@ describe Her::API do
30
33
  before do
31
34
  class SimpleParser < Faraday::Response::Middleware
32
35
  def on_complete(env)
33
- env[:body] = { :data => env[:body] }
36
+ env[:body] = { data: env[:body] }
34
37
  end
35
38
  end
36
39
  end
37
40
 
38
41
  context "making HTTP requests" do
39
- let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo")[:parsed_data] }
42
+ let(:parsed_data) { subject.request(_method: :get, _path: "/foo")[:parsed_data] }
40
43
  before do
41
- subject.setup :url => "https://api.example.com" do |builder|
44
+ subject.setup url: "https://api.example.com" do |builder|
42
45
  builder.use SimpleParser
43
- builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is."] } }
46
+ builder.adapter(:test) { |stub| stub.get("/foo") { [200, {}, "Foo, it is."] } }
44
47
  end
45
48
  end
46
49
 
47
- specify { parsed_data[:data].should == "Foo, it is." }
50
+ specify { expect(parsed_data[:data]).to eq("Foo, it is.") }
48
51
  end
49
52
 
50
53
  context "making HTTP requests while specifying custom HTTP headers" do
51
- let(:parsed_data) { subject.request(:_method => :get, :_path => "/foo", :_headers => { "X-Page" => 2 })[:parsed_data] }
54
+ let(:parsed_data) { subject.request(_method: :get, _path: "/foo", _headers: { "X-Page" => 2 })[:parsed_data] }
52
55
 
53
56
  before do
54
- subject.setup :url => "https://api.example.com" do |builder|
57
+ subject.setup url: "https://api.example.com" do |builder|
55
58
  builder.use SimpleParser
56
- builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is page #{env[:request_headers]["X-Page"]}."] } }
59
+ builder.adapter(:test) { |stub| stub.get("/foo") { |env| [200, {}, "Foo, it is page #{env[:request_headers]['X-Page']}."] } }
57
60
  end
58
61
  end
59
62
 
60
- specify { parsed_data[:data].should == "Foo, it is page 2." }
63
+ specify { expect(parsed_data[:data]).to eq("Foo, it is page 2.") }
61
64
  end
62
65
 
63
66
  context "parsing a request with the default parser" do
64
- let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
67
+ let(:parsed_data) { subject.request(_method: :get, _path: "users/1")[:parsed_data] }
65
68
  before do
66
- subject.setup :url => "https://api.example.com" do |builder|
69
+ subject.setup url: "https://api.example.com" do |builder|
67
70
  builder.use Her::Middleware::FirstLevelParseJSON
68
71
  builder.adapter :test do |stub|
69
- stub.get("/users/1") { |env| [200, {}, MultiJson.dump({ :id => 1, :name => "George Michael Bluth", :errors => ["This is a single error"], :metadata => { :page => 1, :per_page => 10 } })] }
72
+ stub.get("/users/1") { [200, {}, MultiJson.dump(id: 1, name: "George Michael Bluth", errors: ["This is a single error"], metadata: { page: 1, per_page: 10 })] }
70
73
  end
71
74
  end
72
75
  end
73
76
 
74
77
  specify do
75
- parsed_data[:data].should == { :id => 1, :name => "George Michael Bluth" }
76
- parsed_data[:errors].should == ["This is a single error"]
77
- parsed_data[:metadata].should == { :page => 1, :per_page => 10 }
78
+ expect(parsed_data[:data]).to eq(id: 1, name: "George Michael Bluth")
79
+ expect(parsed_data[:errors]).to eq(["This is a single error"])
80
+ expect(parsed_data[:metadata]).to eq(page: 1, per_page: 10)
78
81
  end
79
82
  end
80
83
 
81
84
  context "parsing a request with a custom parser" do
82
- let(:parsed_data) { subject.request(:_method => :get, :_path => "users/1")[:parsed_data] }
85
+ let(:parsed_data) { subject.request(_method: :get, _path: "users/1")[:parsed_data] }
83
86
  before do
84
87
  class CustomParser < Faraday::Response::Middleware
85
88
  def on_complete(env)
86
- json = MultiJson.load(env[:body], :symbolize_keys => true)
89
+ json = MultiJson.load(env[:body], symbolize_keys: true)
87
90
  errors = json.delete(:errors) || []
88
91
  metadata = json.delete(:metadata) || {}
89
92
  env[:body] = {
90
- :data => json,
91
- :errors => errors,
92
- :metadata => metadata,
93
+ data: json,
94
+ errors: errors,
95
+ metadata: metadata
93
96
  }
94
97
  end
95
98
  end
96
99
 
97
- subject.setup :url => "https://api.example.com" do |builder|
100
+ subject.setup url: "https://api.example.com" do |builder|
98
101
  builder.use CustomParser
99
102
  builder.use Faraday::Request::UrlEncoded
100
103
  builder.adapter :test do |stub|
101
- stub.get("/users/1") { |env| [200, {}, MultiJson.dump(:id => 1, :name => "George Michael Bluth")] }
104
+ stub.get("/users/1") { [200, {}, MultiJson.dump(id: 1, name: "George Michael Bluth")] }
102
105
  end
103
106
  end
104
107
  end
105
108
 
106
109
  specify do
107
- parsed_data[:data].should == { :id => 1, :name => "George Michael Bluth" }
108
- parsed_data[:errors].should == []
109
- parsed_data[:metadata].should == {}
110
+ expect(parsed_data[:data]).to eq(id: 1, name: "George Michael Bluth")
111
+ expect(parsed_data[:errors]).to eq([])
112
+ expect(parsed_data[:metadata]).to eq({})
110
113
  end
111
114
  end
112
115
  end
@@ -1,26 +1,41 @@
1
- require 'spec_helper'
1
+ require "spec_helper"
2
2
 
3
3
  describe Her::Collection do
4
-
5
4
  let(:items) { [1, 2, 3, 4] }
6
- let(:metadata) { { :name => 'Testname' } }
7
- let(:errors) { { :name => ['not_present'] } }
5
+ let(:metadata) { { name: "Testname" } }
6
+ let(:errors) { { name: ["not_present"] } }
8
7
 
9
8
  describe "#new" do
10
9
  context "without parameters" do
11
10
  subject { Her::Collection.new }
12
11
 
13
- it { should eq([]) }
14
- its(:metadata) { should eq({}) }
15
- its(:errors) { should eq({}) }
12
+ it { is_expected.to eq([]) }
13
+
14
+ describe "#metadata" do
15
+ subject { super().metadata }
16
+ it { is_expected.to eq({}) }
17
+ end
18
+
19
+ describe "#errors" do
20
+ subject { super().errors }
21
+ it { is_expected.to eq({}) }
22
+ end
16
23
  end
17
24
 
18
25
  context "with parameters" do
19
26
  subject { Her::Collection.new(items, metadata, errors) }
20
27
 
21
- it { should eq([1,2,3,4]) }
22
- its(:metadata) { should eq({ :name => 'Testname' }) }
23
- its(:errors) { should eq({ :name => ['not_present'] }) }
28
+ it { is_expected.to eq([1, 2, 3, 4]) }
29
+
30
+ describe "#metadata" do
31
+ subject { super().metadata }
32
+ it { is_expected.to eq(name: "Testname") }
33
+ end
34
+
35
+ describe "#errors" do
36
+ subject { super().errors }
37
+ it { is_expected.to eq(name: ["not_present"]) }
38
+ end
24
39
  end
25
40
  end
26
41
  end