graphiti 1.0.alpha.5 → 1.0.alpha.6
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/lib/generators/graphiti/api_test_generator.rb +71 -0
- data/lib/generators/graphiti/generator_mixin.rb +45 -0
- data/lib/generators/graphiti/resource_generator.rb +99 -0
- data/lib/generators/graphiti/resource_test_generator.rb +57 -0
- data/lib/generators/{jsonapi → graphiti}/templates/application_resource.rb.erb +0 -0
- data/lib/generators/{jsonapi → graphiti}/templates/controller.rb.erb +0 -0
- data/lib/generators/{jsonapi → graphiti}/templates/create_request_spec.rb.erb +3 -3
- data/lib/generators/{jsonapi → graphiti}/templates/destroy_request_spec.rb.erb +5 -5
- data/lib/generators/graphiti/templates/index_request_spec.rb.erb +22 -0
- data/lib/generators/{jsonapi → graphiti}/templates/resource.rb.erb +0 -0
- data/lib/generators/{jsonapi → graphiti}/templates/resource_reads_spec.rb.erb +12 -12
- data/lib/generators/{jsonapi → graphiti}/templates/resource_writes_spec.rb.erb +12 -12
- data/lib/generators/graphiti/templates/show_request_spec.rb.erb +21 -0
- data/lib/generators/{jsonapi → graphiti}/templates/update_request_spec.rb.erb +5 -5
- data/lib/graphiti.rb +0 -6
- data/lib/graphiti/adapters/abstract.rb +0 -1
- data/lib/graphiti/adapters/active_record/inferrence.rb +8 -1
- data/lib/graphiti/base.rb +1 -1
- data/lib/graphiti/errors.rb +70 -5
- data/lib/graphiti/jsonapi_serializable_ext.rb +18 -6
- data/lib/graphiti/query.rb +28 -17
- data/lib/graphiti/resource.rb +14 -0
- data/lib/graphiti/resource/configuration.rb +22 -3
- data/lib/graphiti/resource/dsl.rb +17 -2
- data/lib/graphiti/resource/interface.rb +10 -8
- data/lib/graphiti/resource/links.rb +6 -3
- data/lib/graphiti/runner.rb +2 -1
- data/lib/graphiti/schema.rb +33 -5
- data/lib/graphiti/schema_diff.rb +71 -1
- data/lib/graphiti/scope.rb +4 -9
- data/lib/graphiti/scoping/base.rb +2 -2
- data/lib/graphiti/scoping/filter.rb +5 -0
- data/lib/graphiti/scoping/filterable.rb +21 -6
- data/lib/graphiti/scoping/paginate.rb +4 -4
- data/lib/graphiti/scoping/sort.rb +26 -5
- data/lib/graphiti/sideload.rb +13 -22
- data/lib/graphiti/sideload/belongs_to.rb +11 -2
- data/lib/graphiti/sideload/has_many.rb +1 -1
- data/lib/graphiti/sideload/polymorphic_belongs_to.rb +2 -3
- data/lib/graphiti/types.rb +1 -1
- data/lib/graphiti/util/class.rb +5 -2
- data/lib/graphiti/util/persistence.rb +1 -1
- data/lib/graphiti/version.rb +1 -1
- metadata +16 -13
- data/lib/generators/jsonapi/resource_generator.rb +0 -169
- data/lib/generators/jsonapi/templates/index_request_spec.rb.erb +0 -22
- data/lib/generators/jsonapi/templates/show_request_spec.rb.erb +0 -21
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'rails_helper'
|
2
|
+
|
3
|
+
RSpec.describe "<%= type %>#show", type: :request do
|
4
|
+
let(:params) { {} }
|
5
|
+
|
6
|
+
subject(:make_request) do
|
7
|
+
jsonapi_get "/<%= api_namespace %>/v1/<%= type %>/#{<%= var %>.id}", params: params
|
8
|
+
end
|
9
|
+
|
10
|
+
describe 'basic fetch' do
|
11
|
+
let!(:<%= var %>) { create(:<%= var %>) }
|
12
|
+
|
13
|
+
it 'works' do
|
14
|
+
expect(<%= resource_class %>).to receive(:find).and_call_original
|
15
|
+
make_request
|
16
|
+
expect(response.status).to eq(200)
|
17
|
+
expect(d.jsonapi_type).to eq('<%= type %>')
|
18
|
+
expect(d.id).to eq(<%= var %>.id)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -2,16 +2,16 @@ require 'rails_helper'
|
|
2
2
|
|
3
3
|
RSpec.describe "<%= type %>#update", type: :request do
|
4
4
|
subject(:make_request) do
|
5
|
-
jsonapi_put "/<%= api_namespace %>/v1/<%= type %>/#{<%=
|
5
|
+
jsonapi_put "/<%= api_namespace %>/v1/<%= type %>/#{<%= var %>.id}", payload
|
6
6
|
end
|
7
7
|
|
8
8
|
describe 'basic update' do
|
9
|
-
let!(:<%=
|
9
|
+
let!(:<%= var %>) { create(:<%= var %>) }
|
10
10
|
|
11
11
|
let(:payload) do
|
12
12
|
{
|
13
13
|
data: {
|
14
|
-
id: <%=
|
14
|
+
id: <%= var %>.id.to_s,
|
15
15
|
type: '<%= type %>',
|
16
16
|
attributes: {
|
17
17
|
# ... your attrs here
|
@@ -22,10 +22,10 @@ RSpec.describe "<%= type %>#update", type: :request do
|
|
22
22
|
|
23
23
|
# Replace 'xit' with 'it' after adding attributes
|
24
24
|
xit 'updates the resource' do
|
25
|
-
expect(<%=
|
25
|
+
expect(<%= resource_class %>).to receive(:find).and_call_original
|
26
26
|
expect {
|
27
27
|
make_request
|
28
|
-
}.to change { <%=
|
28
|
+
}.to change { <%= var %>.reload.attributes }
|
29
29
|
expect(response.status).to eq(200)
|
30
30
|
|
31
31
|
# ... assert updates attributes ...
|
data/lib/graphiti.rb
CHANGED
@@ -475,7 +475,6 @@ module Graphiti
|
|
475
475
|
def activerecord_associate?(parent, child, association_name)
|
476
476
|
defined?(::ActiveRecord) &&
|
477
477
|
parent.is_a?(::ActiveRecord::Base) &&
|
478
|
-
child.is_a?(::ActiveRecord::Base) &&
|
479
478
|
parent.class.reflect_on_association(association_name)
|
480
479
|
end
|
481
480
|
end
|
@@ -2,9 +2,16 @@ module Graphiti
|
|
2
2
|
module Adapters
|
3
3
|
module ActiveRecord
|
4
4
|
module Inferrence
|
5
|
+
# If going AR to AR, use AR introspection
|
6
|
+
# If going AR to PORO, fall back to normal inferrence
|
5
7
|
def infer_foreign_key
|
6
8
|
parent_model = parent_resource_class.model
|
7
|
-
parent_model.reflections[association_name.to_s]
|
9
|
+
reflection = parent_model.reflections[association_name.to_s]
|
10
|
+
if reflection
|
11
|
+
reflection.foreign_key.to_sym
|
12
|
+
else
|
13
|
+
super
|
14
|
+
end
|
8
15
|
end
|
9
16
|
end
|
10
17
|
end
|
data/lib/graphiti/base.rb
CHANGED
data/lib/graphiti/errors.rb
CHANGED
@@ -16,6 +16,53 @@ The adapter #{@adapter.class} does not implement method '#{@method}', which was
|
|
16
16
|
end
|
17
17
|
end
|
18
18
|
|
19
|
+
class SingularSideload < Base
|
20
|
+
def initialize(sideload, parent_length)
|
21
|
+
@sideload = sideload
|
22
|
+
@parent_length = parent_length
|
23
|
+
end
|
24
|
+
|
25
|
+
def message
|
26
|
+
<<-MSG
|
27
|
+
#{@sideload.parent_resource.class.name}: tried to sideload #{@sideload.name.inspect}, but more than one #{@sideload.parent_resource.model.name} was passed!
|
28
|
+
|
29
|
+
This is because you marked the sideload #{@sideload.name.inspect} with single: true
|
30
|
+
|
31
|
+
You might have done this because the sideload can only be loaded from a :show endpoint, and :index would be too expensive.
|
32
|
+
|
33
|
+
Remove the single: true option to bypass this error.
|
34
|
+
MSG
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class UnsupportedSort < Base
|
39
|
+
def initialize(resource, attribute, whitelist, direction)
|
40
|
+
@resource = resource
|
41
|
+
@attribute = attribute
|
42
|
+
@whitelist = whitelist
|
43
|
+
@direction = direction
|
44
|
+
end
|
45
|
+
|
46
|
+
def message
|
47
|
+
<<-MSG
|
48
|
+
#{@resource.class.name}: tried to sort on attribute #{@attribute.inspect}, but passed #{@direction.inspect} when only #{@whitelist.inspect} is supported.
|
49
|
+
MSG
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
class ExtraAttributeNotFound < Base
|
54
|
+
def initialize(resource_class, name)
|
55
|
+
@resource_class = resource_class
|
56
|
+
@name = name
|
57
|
+
end
|
58
|
+
|
59
|
+
def message
|
60
|
+
<<-MSG
|
61
|
+
#{@resource_class.name}: called .on_extra_attribute #{@name.inspect}, but extra attribute #{@name.inspect} does not exist!
|
62
|
+
MSG
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
19
66
|
class InvalidFilterValue < Base
|
20
67
|
def initialize(resource, filter, value)
|
21
68
|
@resource = resource
|
@@ -158,7 +205,7 @@ Graphiti.config.context_for_endpoint = ->(path, action) { ... }
|
|
158
205
|
|
159
206
|
Either set a primary endpoint for this resource:
|
160
207
|
|
161
|
-
|
208
|
+
primary_endpoint '/my/url', [:index, :show, :create]
|
162
209
|
|
163
210
|
Or whitelist a secondary endpoint:
|
164
211
|
|
@@ -347,16 +394,34 @@ Expecting filter #{@filter.inspect} on #{@sideload.resource.class.name}.
|
|
347
394
|
end
|
348
395
|
end
|
349
396
|
|
397
|
+
class MissingDependentFilter < Base
|
398
|
+
def initialize(resource, filters)
|
399
|
+
@resource = resource
|
400
|
+
@filters = filters
|
401
|
+
end
|
402
|
+
|
403
|
+
def message
|
404
|
+
msg = "#{@resource.class.name}: The following filters had dependencies that were not passed:"
|
405
|
+
@filters.each do |f|
|
406
|
+
msg << "\n#{f[:filter][:name].inspect} - dependent on #{f[:filter][:dependencies].inspect}, but #{f[:missing].inspect} not passed."
|
407
|
+
end
|
408
|
+
msg
|
409
|
+
end
|
410
|
+
end
|
411
|
+
|
350
412
|
class ResourceNotFound < Base
|
351
|
-
def initialize(resource_class, sideload_name)
|
413
|
+
def initialize(resource_class, sideload_name, tried)
|
352
414
|
@resource_class = resource_class
|
353
415
|
@sideload_name = sideload_name
|
416
|
+
@tried = tried
|
354
417
|
end
|
355
418
|
|
356
419
|
def message
|
357
420
|
<<-MSG
|
358
421
|
Could not find resource class for sideload '#{@sideload_name}' on Resource '#{@resource_class.name}'!
|
359
422
|
|
423
|
+
Tried to find classes: #{@tried.join(', ')}
|
424
|
+
|
360
425
|
If this follows a non-standard naming convention, use the :resource option to pass it directly:
|
361
426
|
|
362
427
|
has_many :comments, resource: SpecialCommentResource
|
@@ -395,13 +460,13 @@ Consider using a named relationship instead, e.g. 'has_one :top_comment'
|
|
395
460
|
end
|
396
461
|
|
397
462
|
class InvalidInclude < Base
|
398
|
-
def initialize(
|
463
|
+
def initialize(resource_class, relationship)
|
464
|
+
@resource_class = resource_class
|
399
465
|
@relationship = relationship
|
400
|
-
@parent_resource = parent_resource
|
401
466
|
end
|
402
467
|
|
403
468
|
def message
|
404
|
-
|
469
|
+
"#{@resource_class.name}: The requested included relationship \"#{@relationship}\" is not supported."
|
405
470
|
end
|
406
471
|
end
|
407
472
|
|
@@ -7,8 +7,9 @@ module Graphiti
|
|
7
7
|
# To ensure we always render with the *resource* serializer
|
8
8
|
module RendererOverrides
|
9
9
|
def _build(object, exposures, klass)
|
10
|
-
|
11
|
-
klass.
|
10
|
+
resource = object.instance_variable_get(:@__graphiti_resource)
|
11
|
+
klass = object.instance_variable_get(:@__graphiti_serializer)
|
12
|
+
klass.new(exposures.merge(object: object, resource: resource))
|
12
13
|
end
|
13
14
|
end
|
14
15
|
|
@@ -21,12 +22,14 @@ module Graphiti
|
|
21
22
|
nil
|
22
23
|
elsif resources.respond_to?(:to_ary)
|
23
24
|
Array(resources).map do |obj|
|
24
|
-
klass = obj.instance_variable_get(:@
|
25
|
-
|
25
|
+
klass = obj.instance_variable_get(:@__graphiti_serializer)
|
26
|
+
resource = obj.instance_variable_get(:@__graphiti_resource)
|
27
|
+
klass.new(@_exposures.merge(object: obj, resource: resource))
|
26
28
|
end
|
27
29
|
else
|
28
|
-
klass = resources.instance_variable_get(:@
|
29
|
-
|
30
|
+
klass = resources.instance_variable_get(:@__graphiti_serializer)
|
31
|
+
resource = resources.instance_variable_get(:@__graphiti_resource)
|
32
|
+
klass.new(@_exposures.merge(object: resources, resource: resource))
|
30
33
|
end
|
31
34
|
end
|
32
35
|
end
|
@@ -38,6 +41,15 @@ module Graphiti
|
|
38
41
|
def requested_relationships(fields)
|
39
42
|
@_relationships
|
40
43
|
end
|
44
|
+
|
45
|
+
# Allow access to resource methods
|
46
|
+
def method_missing(id, *args, &blk)
|
47
|
+
if @resource.respond_to?(id, true)
|
48
|
+
@resource.send(id, *args, &blk)
|
49
|
+
else
|
50
|
+
super
|
51
|
+
end
|
52
|
+
end
|
41
53
|
end
|
42
54
|
|
43
55
|
JSONAPI::Serializable::Resource
|
data/lib/graphiti/query.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
module Graphiti
|
2
2
|
class Query
|
3
|
-
attr_reader :resource, :include_hash, :association_name
|
3
|
+
attr_reader :resource, :include_hash, :association_name, :params
|
4
4
|
|
5
5
|
def initialize(resource, params, association_name = nil, nested_include = nil, parents = [])
|
6
6
|
@resource = resource
|
@@ -29,17 +29,17 @@ module Graphiti
|
|
29
29
|
end
|
30
30
|
end
|
31
31
|
|
32
|
-
def
|
33
|
-
{}.tap do |
|
34
|
-
|
35
|
-
|
36
|
-
|
32
|
+
def hash
|
33
|
+
@hash ||= {}.tap do |h|
|
34
|
+
h[:filter] = filters unless filters.empty?
|
35
|
+
h[:sort] = sorts unless sorts.empty?
|
36
|
+
h[:page] = pagination unless pagination.empty?
|
37
37
|
unless association?
|
38
|
-
|
39
|
-
|
38
|
+
h[:fields] = fields unless fields.empty?
|
39
|
+
h[:extra_fields] = extra_fields unless extra_fields.empty?
|
40
40
|
end
|
41
|
-
|
42
|
-
|
41
|
+
h[:stats] = stats unless stats.empty?
|
42
|
+
h[:include] = sideload_hash unless sideload_hash.empty?
|
43
43
|
end
|
44
44
|
end
|
45
45
|
|
@@ -53,7 +53,7 @@ module Graphiti
|
|
53
53
|
@sideload_hash = begin
|
54
54
|
{}.tap do |hash|
|
55
55
|
sideloads.each_pair do |key, value|
|
56
|
-
hash[key] = sideloads[key].
|
56
|
+
hash[key] = sideloads[key].hash
|
57
57
|
end
|
58
58
|
end
|
59
59
|
end
|
@@ -66,6 +66,7 @@ module Graphiti
|
|
66
66
|
sideload = @resource.class.sideload(key)
|
67
67
|
if sideload
|
68
68
|
_parents = parents + [self]
|
69
|
+
sub_hash = sub_hash[:include] if sub_hash.has_key?(:include)
|
69
70
|
hash[key] = Query.new(sideload.resource, @params, key, sub_hash, _parents)
|
70
71
|
else
|
71
72
|
handle_missing_sideload(key)
|
@@ -163,6 +164,8 @@ module Graphiti
|
|
163
164
|
|
164
165
|
whitelist ? Util::IncludeParams.scrub(requested, whitelist) : requested
|
165
166
|
end
|
167
|
+
|
168
|
+
@include_hash
|
166
169
|
end
|
167
170
|
|
168
171
|
def stats
|
@@ -182,17 +185,25 @@ module Graphiti
|
|
182
185
|
|
183
186
|
private
|
184
187
|
|
188
|
+
# Try to find on this resource
|
189
|
+
# If not there, follow the legacy logic of scalling all other
|
190
|
+
# resource names/types
|
191
|
+
# TODO: Eventually, remove the legacy logic
|
185
192
|
def validate!(name, flag)
|
186
193
|
return false if name.to_s.include?('.') # nested
|
187
194
|
|
188
|
-
|
189
|
-
|
195
|
+
if att = @resource.get_attr(name, flag, request: true)
|
196
|
+
return att
|
197
|
+
else
|
198
|
+
not_associated_name = !@resource.class.association_names.include?(name)
|
199
|
+
not_associated_type = !@resource.class.association_types.include?(name)
|
190
200
|
|
191
|
-
|
192
|
-
|
193
|
-
|
201
|
+
if not_associated_name && not_associated_type
|
202
|
+
@resource.get_attr!(name, flag, request: true)
|
203
|
+
return true
|
204
|
+
end
|
205
|
+
false
|
194
206
|
end
|
195
|
-
false
|
196
207
|
end
|
197
208
|
|
198
209
|
def nested?(name)
|
data/lib/graphiti/resource.rb
CHANGED
@@ -9,6 +9,14 @@ module Graphiti
|
|
9
9
|
attr_reader :context
|
10
10
|
|
11
11
|
def around_scoping(scope, query_hash)
|
12
|
+
extra_fields = query_hash[:extra_fields] || {}
|
13
|
+
extra_fields = extra_fields[type] || []
|
14
|
+
extra_fields.each do |name|
|
15
|
+
if config = self.class.config[:extra_attributes][name]
|
16
|
+
scope = instance_exec(scope, &config[:hook]) if config[:hook]
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
12
20
|
yield scope
|
13
21
|
end
|
14
22
|
|
@@ -16,6 +24,12 @@ module Graphiti
|
|
16
24
|
serializer
|
17
25
|
end
|
18
26
|
|
27
|
+
def decorate_record(record)
|
28
|
+
serializer = serializer_for(record)
|
29
|
+
record.instance_variable_set(:@__graphiti_serializer, serializer)
|
30
|
+
record.instance_variable_set(:@__graphiti_resource, self)
|
31
|
+
end
|
32
|
+
|
19
33
|
def with_context(object, namespace = nil)
|
20
34
|
Graphiti.with_context(object, namespace) do
|
21
35
|
yield
|
@@ -3,6 +3,8 @@ module Graphiti
|
|
3
3
|
module Configuration
|
4
4
|
extend ActiveSupport::Concern
|
5
5
|
|
6
|
+
DEFAULT_MAX_PAGE_SIZE = 1_000
|
7
|
+
|
6
8
|
module Overrides
|
7
9
|
def serializer=(val)
|
8
10
|
if val
|
@@ -52,6 +54,7 @@ module Graphiti
|
|
52
54
|
:serializer,
|
53
55
|
:default_page_size,
|
54
56
|
:default_sort,
|
57
|
+
:max_page_size,
|
55
58
|
:attributes_readable_by_default,
|
56
59
|
:attributes_writable_by_default,
|
57
60
|
:attributes_sortable_by_default,
|
@@ -67,13 +70,12 @@ module Graphiti
|
|
67
70
|
super
|
68
71
|
klass.config = Util::Hash.deep_dup(config)
|
69
72
|
klass.adapter ||= Adapters::Abstract
|
70
|
-
klass.
|
71
|
-
klass.default_page_size ||= 20
|
73
|
+
klass.max_page_size ||= DEFAULT_MAX_PAGE_SIZE
|
72
74
|
# re-assigning causes a new Class.new
|
73
75
|
if klass.serializer
|
74
76
|
klass.serializer = klass.serializer
|
75
77
|
else
|
76
|
-
klass.serializer =
|
78
|
+
klass.serializer = klass.infer_serializer_superclass
|
77
79
|
end
|
78
80
|
klass.type ||= klass.infer_type
|
79
81
|
default(klass, :attributes_readable_by_default, true)
|
@@ -130,6 +132,23 @@ module Graphiti
|
|
130
132
|
name.gsub('Resource', '').safe_constantize if name
|
131
133
|
end
|
132
134
|
|
135
|
+
# @api private
|
136
|
+
def infer_serializer_superclass
|
137
|
+
serializer_class = JSONAPI::Serializable::Resource
|
138
|
+
namespace = Util::Class.namespace_for(self)
|
139
|
+
app_serializer = "#{namespace}::ApplicationSerializer"
|
140
|
+
.safe_constantize
|
141
|
+
app_serializer ||= "ApplicationSerializer".safe_constantize
|
142
|
+
|
143
|
+
if app_serializer
|
144
|
+
if app_serializer.ancestors.include?(serializer_class)
|
145
|
+
serializer_class = app_serializer
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
serializer_class
|
150
|
+
end
|
151
|
+
|
133
152
|
def default(object, attr, value)
|
134
153
|
prior = object.send(attr)
|
135
154
|
unless prior || prior == false
|
@@ -15,12 +15,16 @@ module Graphiti
|
|
15
15
|
opts[:single] = true
|
16
16
|
end
|
17
17
|
|
18
|
+
required = att[:filterable] == :required || !!opts[:required]
|
18
19
|
config[:filters][name.to_sym] = {
|
19
20
|
aliases: aliases,
|
21
|
+
name: name.to_sym,
|
20
22
|
type: att[:type],
|
21
23
|
allow: opts[:allow],
|
22
24
|
reject: opts[:reject],
|
23
25
|
single: !!opts[:single],
|
26
|
+
dependencies: opts[:dependent],
|
27
|
+
required: required,
|
24
28
|
operators: operators.to_hash
|
25
29
|
}
|
26
30
|
else
|
@@ -45,7 +49,9 @@ module Graphiti
|
|
45
49
|
opts = args.extract_options!
|
46
50
|
|
47
51
|
if get_attr(name, :sortable, raise_error: :only_unsupported)
|
48
|
-
config[:sorts][name] =
|
52
|
+
config[:sorts][name] = {
|
53
|
+
proc: blk
|
54
|
+
}.merge(opts.slice(:only))
|
49
55
|
else
|
50
56
|
if type = args[0]
|
51
57
|
attribute name, type, only: [:sortable]
|
@@ -89,7 +95,8 @@ module Graphiti
|
|
89
95
|
options[:proc] = blk
|
90
96
|
config[:attributes][name] = options
|
91
97
|
apply_attributes_to_serializer
|
92
|
-
filter(name)
|
98
|
+
options[:filterable] ? filter(name) : config[:filters].delete(name)
|
99
|
+
options[:sortable] ? sort(name) : config[:sorts].delete(name)
|
93
100
|
end
|
94
101
|
|
95
102
|
def extra_attribute(name, type, options = {}, &blk)
|
@@ -107,6 +114,14 @@ module Graphiti
|
|
107
114
|
apply_extra_attributes_to_serializer
|
108
115
|
end
|
109
116
|
|
117
|
+
def on_extra_attribute(name, &blk)
|
118
|
+
if config[:extra_attributes][name]
|
119
|
+
config[:extra_attributes][name][:hook] = blk
|
120
|
+
else
|
121
|
+
raise Errors::ExtraAttributeNotFound.new(self, name)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
110
125
|
def all_attributes
|
111
126
|
attributes.merge(extra_attributes)
|
112
127
|
end
|