graphiti 1.0.alpha.5 → 1.0.alpha.6
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.
- 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
|