jsonapionify 0.9.0 → 0.9.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.
- checksums.yaml +13 -5
- data/.rubocop.yml +1 -0
- data/.ruby-version +1 -1
- data/.travis.yml +8 -0
- data/README.md +85 -3
- data/Rakefile +14 -0
- data/jsonapionify.gemspec +3 -0
- data/lib/jsonapionify/api/action.rb +84 -121
- data/lib/jsonapionify/api/attribute.rb +97 -20
- data/lib/jsonapionify/api/base/class_methods.rb +5 -4
- data/lib/jsonapionify/api/base/delegation.rb +20 -4
- data/lib/jsonapionify/api/base/doc_helper.rb +3 -3
- data/lib/jsonapionify/api/base/reloader.rb +1 -1
- data/lib/jsonapionify/api/base/resource_definitions.rb +28 -15
- data/lib/jsonapionify/api/base.rb +6 -0
- data/lib/jsonapionify/api/context.rb +18 -5
- data/lib/jsonapionify/api/context_delegate.rb +24 -7
- data/lib/jsonapionify/api/errors.rb +2 -0
- data/lib/jsonapionify/api/errors_object.rb +6 -5
- data/lib/jsonapionify/api/relationship/blocks.rb +1 -1
- data/lib/jsonapionify/api/relationship/many.rb +35 -11
- data/lib/jsonapionify/api/relationship/one.rb +17 -7
- data/lib/jsonapionify/api/relationship.rb +20 -6
- data/lib/jsonapionify/api/resource/builders.rb +81 -30
- data/lib/jsonapionify/api/resource/caching.rb +28 -0
- data/lib/jsonapionify/api/resource/caller.rb +61 -0
- data/lib/jsonapionify/api/resource/class_methods.rb +6 -2
- data/lib/jsonapionify/api/resource/defaults/actions.rb +47 -0
- data/lib/jsonapionify/api/resource/defaults/errors.rb +61 -15
- data/lib/jsonapionify/api/resource/defaults/hooks.rb +68 -0
- data/lib/jsonapionify/api/resource/defaults/options.rb +16 -28
- data/lib/jsonapionify/api/resource/defaults/params.rb +3 -0
- data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +80 -32
- data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +13 -6
- data/lib/jsonapionify/api/resource/defaults.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/actions.rb +81 -55
- data/lib/jsonapionify/api/resource/definitions/attributes.rb +46 -10
- data/lib/jsonapionify/api/resource/definitions/contexts.rb +6 -2
- data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/pagination.rb +47 -56
- data/lib/jsonapionify/api/resource/definitions/params.rb +11 -15
- data/lib/jsonapionify/api/resource/definitions/relationships.rb +43 -7
- data/lib/jsonapionify/api/resource/definitions/request_headers.rb +6 -3
- data/lib/jsonapionify/api/resource/definitions/response_headers.rb +1 -1
- data/lib/jsonapionify/api/resource/definitions/scopes.rb +5 -5
- data/lib/jsonapionify/api/resource/definitions/sorting.rb +12 -11
- data/lib/jsonapionify/api/resource/definitions.rb +1 -1
- data/lib/jsonapionify/api/resource/error_handling.rb +92 -20
- data/lib/jsonapionify/api/resource/exec.rb +11 -0
- data/lib/jsonapionify/api/resource/includer.rb +89 -1
- data/lib/jsonapionify/api/resource.rb +55 -8
- data/lib/jsonapionify/api/response.rb +43 -14
- data/lib/jsonapionify/api/server/media_type.rb +36 -0
- data/lib/jsonapionify/api/server/request.rb +25 -11
- data/lib/jsonapionify/api/server.rb +8 -4
- data/lib/jsonapionify/api/sort_field.rb +18 -0
- data/lib/jsonapionify/api/sort_field_set.rb +1 -1
- data/lib/jsonapionify/api/test_helper.rb +46 -0
- data/lib/jsonapionify/documentation/template.erb +2 -2
- data/lib/jsonapionify/documentation.rb +10 -0
- data/lib/jsonapionify/structure/collections/base.rb +10 -3
- data/lib/jsonapionify/structure/helpers/object_defaults.rb +5 -10
- data/lib/jsonapionify/structure/maps/relationships.rb +4 -0
- data/lib/jsonapionify/structure/objects/attributes.rb +4 -0
- data/lib/jsonapionify/structure/objects/base.rb +22 -9
- data/lib/jsonapionify/structure/objects/error.rb +2 -0
- data/lib/jsonapionify/structure/objects/jsonapi.rb +1 -0
- data/lib/jsonapionify/structure/objects/link.rb +1 -0
- data/lib/jsonapionify/structure/objects/relationship.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource.rb +2 -0
- data/lib/jsonapionify/structure/objects/resource_identifier.rb +12 -4
- data/lib/jsonapionify/structure/objects/top_level.rb +4 -2
- data/lib/jsonapionify/types/array_type.rb +16 -11
- data/lib/jsonapionify/types/boolean_type.rb +9 -4
- data/lib/jsonapionify/types/date_string_type.rb +7 -10
- data/lib/jsonapionify/types/float_type.rb +13 -0
- data/lib/jsonapionify/types/integer_type.rb +12 -0
- data/lib/jsonapionify/types/object_type.rb +7 -2
- data/lib/jsonapionify/types/string_type.rb +12 -0
- data/lib/jsonapionify/types/time_string_type.rb +8 -10
- data/lib/jsonapionify/types.rb +43 -5
- data/lib/jsonapionify/version.rb +1 -1
- data/lib/jsonapionify.rb +36 -1
- metadata +121 -74
|
@@ -17,33 +17,46 @@ module JSONAPIonify::Api
|
|
|
17
17
|
klass = Class.new(resource_class, &resource_definitions[type]).set_type(type)
|
|
18
18
|
param(:fields, type)
|
|
19
19
|
const_set const_name, klass
|
|
20
|
-
rescue
|
|
21
|
-
raise e
|
|
22
|
-
|
|
23
|
-
retry
|
|
20
|
+
rescue NameError => e
|
|
21
|
+
raise e unless e.instance_of?(NameError)
|
|
22
|
+
raise Errors::ResourceNotFound, "Resource not defined: #{type}"
|
|
24
23
|
end
|
|
25
24
|
|
|
26
25
|
def resource_defined?(name)
|
|
26
|
+
load_resources
|
|
27
27
|
!!resource_definitions[name]
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def resources
|
|
31
|
+
load_resources
|
|
31
32
|
resource_definitions.map do |name, _|
|
|
32
33
|
resource(name)
|
|
33
34
|
end
|
|
34
35
|
end
|
|
35
36
|
|
|
36
|
-
def define_resource(name, &block)
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
37
|
+
def define_resource(name, extend: nil, &block)
|
|
38
|
+
resource_definitions[name.to_sym] =
|
|
39
|
+
if extend
|
|
40
|
+
sup = superclass
|
|
41
|
+
cur = self
|
|
42
|
+
proc do
|
|
43
|
+
extend_def =
|
|
44
|
+
if name.to_sym == extend.to_sym && sup.respond_to?(:resource)
|
|
45
|
+
sup.resource(extend)
|
|
46
|
+
sup.resource_definitions[extend.to_sym]
|
|
47
|
+
else
|
|
48
|
+
cur.resource(extend)
|
|
49
|
+
cur.resource_definitions[extend.to_sym]
|
|
50
|
+
end
|
|
51
|
+
class_eval &extend_def
|
|
52
|
+
class_eval &block
|
|
53
|
+
end
|
|
54
|
+
else
|
|
55
|
+
block
|
|
56
|
+
end
|
|
57
|
+
const_name = name.to_s.camelcase + 'Resource'
|
|
58
|
+
remove_const(const_name) if const_defined? const_name, false
|
|
59
|
+
name
|
|
47
60
|
end
|
|
48
61
|
|
|
49
62
|
end
|
|
@@ -29,6 +29,12 @@ module JSONAPIonify::Api
|
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
|
|
32
|
+
def self.cache_key(**options)
|
|
33
|
+
Base64.urlsafe_encode64(
|
|
34
|
+
{ **options, dsl: JSONAPIonify.digest, api: signature, }.to_json
|
|
35
|
+
)
|
|
36
|
+
end
|
|
37
|
+
|
|
32
38
|
def self.resource_class
|
|
33
39
|
if const_defined?(:ResourceBase, false)
|
|
34
40
|
const_get(:ResourceBase, false)
|
|
@@ -1,14 +1,27 @@
|
|
|
1
1
|
module JSONAPIonify::Api
|
|
2
|
-
class Context
|
|
2
|
+
class Context
|
|
3
|
+
|
|
4
|
+
def initialize(readonly: false, persisted: false, existing_context: nil, &block)
|
|
5
|
+
@readonly = readonly
|
|
6
|
+
@persisted = persisted
|
|
7
|
+
@existing_context = existing_context
|
|
8
|
+
@block = block
|
|
9
|
+
end
|
|
3
10
|
|
|
4
11
|
def call(instance, delegate)
|
|
5
|
-
|
|
6
|
-
|
|
12
|
+
existing_context = @existing_context || proc {}
|
|
13
|
+
existing_block = proc { existing_context.call(instance, delegate) }
|
|
14
|
+
block = @block || proc {}
|
|
15
|
+
instance.instance_exec(delegate, existing_block, &block)
|
|
7
16
|
end
|
|
8
17
|
|
|
9
18
|
def readonly?
|
|
10
|
-
|
|
19
|
+
!!@readonly
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def persisted?
|
|
23
|
+
!!@persisted
|
|
11
24
|
end
|
|
12
25
|
|
|
13
26
|
end
|
|
14
|
-
end
|
|
27
|
+
end
|
|
@@ -1,15 +1,22 @@
|
|
|
1
1
|
module JSONAPIonify::Api
|
|
2
2
|
class ContextDelegate
|
|
3
3
|
class Mock
|
|
4
|
-
def
|
|
4
|
+
def initialize(**attrs)
|
|
5
|
+
attrs.each do |attr, value|
|
|
6
|
+
define_singleton_method(attr) { value }
|
|
7
|
+
end
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def method_missing(*)
|
|
5
11
|
self
|
|
6
12
|
end
|
|
7
13
|
end
|
|
8
14
|
|
|
9
|
-
def initialize(request, instance, definitions)
|
|
10
|
-
memo
|
|
11
|
-
|
|
12
|
-
|
|
15
|
+
def initialize(request, instance, definitions, **overrides)
|
|
16
|
+
memo = {}
|
|
17
|
+
persisted_memo = {}
|
|
18
|
+
delegate = self
|
|
19
|
+
@definitions = definitions
|
|
13
20
|
|
|
14
21
|
define_singleton_method :request do
|
|
15
22
|
request
|
|
@@ -27,13 +34,23 @@ module JSONAPIonify::Api
|
|
|
27
34
|
memo.delete(key)
|
|
28
35
|
end
|
|
29
36
|
|
|
37
|
+
define_singleton_method(:clear) do
|
|
38
|
+
memo.clear
|
|
39
|
+
end
|
|
40
|
+
|
|
30
41
|
definitions.each do |name, context|
|
|
31
42
|
define_singleton_method name do
|
|
32
|
-
|
|
43
|
+
return persisted_memo[name] if persisted_memo.has_key? name
|
|
44
|
+
(context.persisted? ? persisted_memo : memo)[name] ||=
|
|
45
|
+
if overrides.has_key?(name)
|
|
46
|
+
overrides[name]
|
|
47
|
+
else
|
|
48
|
+
context.call(instance, delegate)
|
|
49
|
+
end
|
|
33
50
|
end
|
|
34
51
|
|
|
35
52
|
define_singleton_method "#{name}=" do |value|
|
|
36
|
-
|
|
53
|
+
persisted_memo[name] = value
|
|
37
54
|
end unless context.readonly?
|
|
38
55
|
end
|
|
39
56
|
end
|
|
@@ -6,6 +6,8 @@ module JSONAPIonify::Api
|
|
|
6
6
|
RequestError = Class.new JSONAPIonifyError
|
|
7
7
|
CacheHit = Class.new JSONAPIonifyError
|
|
8
8
|
DoubleCacheError = Class.new JSONAPIonifyError
|
|
9
|
+
DoubleRespondError = Class.new JSONAPIonifyError
|
|
9
10
|
InvalidCursor = Class.new JSONAPIonifyError
|
|
11
|
+
MissingContentType = Class.new JSONAPIonifyError
|
|
10
12
|
end
|
|
11
13
|
end
|
|
@@ -33,15 +33,16 @@ module JSONAPIonify
|
|
|
33
33
|
end
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
-
def evaluate(*args, error_block:, runtime_block
|
|
37
|
-
backtrace
|
|
38
|
-
|
|
39
|
-
|
|
36
|
+
def evaluate(*args, error_block:, runtime_block: nil, backtrace: nil)
|
|
37
|
+
backtrace ||= caller
|
|
38
|
+
runtime_block ||= proc {}
|
|
39
|
+
error = Structure::Objects::Error.new
|
|
40
|
+
evaluator = Evaluator.new(error)
|
|
40
41
|
collection << error
|
|
41
42
|
[runtime_block, error_block].each do |block|
|
|
42
43
|
evaluator.instance_exec(*args, &block) if block
|
|
43
44
|
end
|
|
44
|
-
|
|
45
|
+
if JSONAPIonify.show_backtrace == true
|
|
45
46
|
error[:meta] ||= {}
|
|
46
47
|
error[:meta][:backtrace] = backtrace
|
|
47
48
|
end
|
|
@@ -8,17 +8,28 @@ module JSONAPIonify::Api
|
|
|
8
8
|
undef_method :read
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
define_singleton_method(:show) do
|
|
12
|
-
options
|
|
11
|
+
define_singleton_method(:show) do |content_type: nil, callbacks: true, &block|
|
|
12
|
+
options = {
|
|
13
|
+
content_type: content_type,
|
|
14
|
+
callbacks: callbacks,
|
|
15
|
+
cacheable: true,
|
|
16
|
+
prepend: 'relationships'
|
|
17
|
+
}
|
|
13
18
|
define_action(:show, 'GET', **options, &block).response status: 200 do |context|
|
|
14
19
|
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
15
20
|
context.response_object.to_json
|
|
16
21
|
end
|
|
17
22
|
end
|
|
18
23
|
|
|
19
|
-
define_singleton_method(:replace) do
|
|
20
|
-
options
|
|
21
|
-
|
|
24
|
+
define_singleton_method(:replace) do |content_type: nil, callbacks: true, &block|
|
|
25
|
+
options = {
|
|
26
|
+
content_type: content_type,
|
|
27
|
+
callbacks: callbacks,
|
|
28
|
+
cacheable: false,
|
|
29
|
+
prepend: 'relationships',
|
|
30
|
+
example_input: :resource_identifier
|
|
31
|
+
}
|
|
32
|
+
define_action(:replace, 'PATCH', **options, &block).response status: 200 do |context|
|
|
22
33
|
context.owner_context.reset(:instance)
|
|
23
34
|
context.reset(:collection)
|
|
24
35
|
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
@@ -26,9 +37,15 @@ module JSONAPIonify::Api
|
|
|
26
37
|
end
|
|
27
38
|
end
|
|
28
39
|
|
|
29
|
-
define_singleton_method(:add) do
|
|
30
|
-
options
|
|
31
|
-
|
|
40
|
+
define_singleton_method(:add) do |content_type: nil, callbacks: true, &block|
|
|
41
|
+
options = {
|
|
42
|
+
content_type: content_type,
|
|
43
|
+
callbacks: callbacks,
|
|
44
|
+
cacheable: false,
|
|
45
|
+
prepend: 'relationships',
|
|
46
|
+
example_input: :resource_identifier
|
|
47
|
+
}
|
|
48
|
+
define_action(:add, 'POST', **options, &block).response status: 200 do |context|
|
|
32
49
|
context.owner_context.reset(:instance)
|
|
33
50
|
context.reset(:collection)
|
|
34
51
|
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
@@ -36,9 +53,16 @@ module JSONAPIonify::Api
|
|
|
36
53
|
end
|
|
37
54
|
end
|
|
38
55
|
|
|
39
|
-
define_singleton_method(:remove) do
|
|
56
|
+
define_singleton_method(:remove) do |content_type: nil, callbacks: true, &block|
|
|
57
|
+
options = {
|
|
58
|
+
content_type: content_type,
|
|
59
|
+
callbacks: callbacks,
|
|
60
|
+
cacheable: false,
|
|
61
|
+
prepend: 'relationships',
|
|
62
|
+
example_input: :resource_identifier
|
|
63
|
+
}
|
|
40
64
|
options[:prepend] = 'relationships'
|
|
41
|
-
define_action(:remove, 'DELETE',
|
|
65
|
+
define_action(:remove, 'DELETE', **options, &block).response status: 200 do |context|
|
|
42
66
|
context.owner_context.reset(:instance)
|
|
43
67
|
context.reset(:collection)
|
|
44
68
|
context.response_object[:data] = build_identifier_collection(context.collection)
|
|
@@ -47,7 +71,7 @@ module JSONAPIonify::Api
|
|
|
47
71
|
end
|
|
48
72
|
|
|
49
73
|
context :scope do |context|
|
|
50
|
-
context.
|
|
74
|
+
instance_exec rel.name, context.owner, context, &rel.resolve
|
|
51
75
|
end
|
|
52
76
|
|
|
53
77
|
show
|
|
@@ -9,17 +9,27 @@ module JSONAPIonify::Api
|
|
|
9
9
|
undef_method :list
|
|
10
10
|
end
|
|
11
11
|
|
|
12
|
-
define_singleton_method(:show) do
|
|
13
|
-
options
|
|
14
|
-
|
|
12
|
+
define_singleton_method(:show) do |content_type: nil, callbacks: true, &block|
|
|
13
|
+
options = {
|
|
14
|
+
content_type: content_type,
|
|
15
|
+
callbacks: callbacks,
|
|
16
|
+
cacheable: true,
|
|
17
|
+
prepend: 'relationships'
|
|
18
|
+
}
|
|
19
|
+
define_action(:show, 'GET', **options, &block).response status: 200 do |context|
|
|
15
20
|
context.response_object[:data] = build_resource_identifier(context.instance)
|
|
16
21
|
context.response_object.to_json
|
|
17
22
|
end
|
|
18
23
|
end
|
|
19
24
|
|
|
20
|
-
define_singleton_method(:replace) do
|
|
21
|
-
options
|
|
22
|
-
|
|
25
|
+
define_singleton_method(:replace) do |content_type: nil, callbacks: true, &block|
|
|
26
|
+
options = {
|
|
27
|
+
content_type: content_type,
|
|
28
|
+
callbacks: callbacks,
|
|
29
|
+
cacheable: false,
|
|
30
|
+
prepend: 'relationships'
|
|
31
|
+
}
|
|
32
|
+
define_action(:replace, 'PATCH', **options, &block).response status: 200 do |context|
|
|
23
33
|
context.owner_context.reset(:instance)
|
|
24
34
|
context.reset(:instance)
|
|
25
35
|
context.response_object[:data] = build_resource_identifier(context.instance)
|
|
@@ -28,7 +38,7 @@ module JSONAPIonify::Api
|
|
|
28
38
|
end
|
|
29
39
|
|
|
30
40
|
context :instance do |context|
|
|
31
|
-
context.
|
|
41
|
+
instance_exec rel.name, context.owner, context, &rel.resolve
|
|
32
42
|
end
|
|
33
43
|
|
|
34
44
|
show
|
|
@@ -23,18 +23,18 @@ module JSONAPIonify::Api
|
|
|
23
23
|
end
|
|
24
24
|
|
|
25
25
|
owner_context_proc = Proc.new do |request|
|
|
26
|
-
|
|
26
|
+
rel.owner.new(request: request).exec { |c| c }
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
context(:owner_context) do |context|
|
|
29
|
+
context(:owner_context, readonly: true, persisted: true) do |context|
|
|
30
30
|
owner_context_proc.call(context.request)
|
|
31
31
|
end
|
|
32
32
|
|
|
33
|
-
context(:owner) do |context|
|
|
33
|
+
context(:owner, readonly: true, persisted: true) do |context|
|
|
34
34
|
context.owner_context.instance
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
-
context(:id) do
|
|
37
|
+
context(:id, readonly: true, persisted: true) do
|
|
38
38
|
nil
|
|
39
39
|
end
|
|
40
40
|
|
|
@@ -62,13 +62,23 @@ module JSONAPIonify::Api
|
|
|
62
62
|
end
|
|
63
63
|
end
|
|
64
64
|
|
|
65
|
-
attr_reader :owner, :class_proc, :name
|
|
65
|
+
attr_reader :owner, :class_proc, :name, :resolve
|
|
66
66
|
|
|
67
|
-
def initialize(owner, name, resource: nil, &block)
|
|
67
|
+
def initialize(owner, name, resource: nil, includable: false, resolve: proc { |name, owner| owner.send(name) }, &block)
|
|
68
68
|
@class_proc = block || proc {}
|
|
69
69
|
@owner = owner
|
|
70
70
|
@name = name
|
|
71
|
+
@includable = includable
|
|
71
72
|
@resource = resource || name
|
|
73
|
+
@resolve = resolve
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def options_json
|
|
77
|
+
{
|
|
78
|
+
name: name,
|
|
79
|
+
type: resource.type,
|
|
80
|
+
relationship_type: self.class.name.split(':').last.downcase
|
|
81
|
+
}
|
|
72
82
|
end
|
|
73
83
|
|
|
74
84
|
def documentation_object
|
|
@@ -97,5 +107,9 @@ module JSONAPIonify::Api
|
|
|
97
107
|
owner.api.resource(@resource)
|
|
98
108
|
end
|
|
99
109
|
|
|
110
|
+
def includable?
|
|
111
|
+
!!@includable
|
|
112
|
+
end
|
|
113
|
+
|
|
100
114
|
end
|
|
101
115
|
end
|
|
@@ -1,51 +1,98 @@
|
|
|
1
1
|
module JSONAPIonify::Api
|
|
2
2
|
module Resource::Builders
|
|
3
3
|
extend ActiveSupport::Concern
|
|
4
|
+
FALSEY_STRINGS = JSONAPIonify::FALSEY_STRINGS
|
|
5
|
+
TRUTHY_STRINGS = JSONAPIonify::TRUTHY_STRINGS
|
|
6
|
+
Structure = JSONAPIonify::Structure
|
|
4
7
|
|
|
5
8
|
module ClassMethods
|
|
6
|
-
|
|
7
|
-
|
|
9
|
+
|
|
10
|
+
def build_resource(
|
|
11
|
+
context,
|
|
12
|
+
instance,
|
|
13
|
+
fields:,
|
|
14
|
+
relationships: true,
|
|
15
|
+
links: true,
|
|
16
|
+
include_cursor: false, &block
|
|
17
|
+
)
|
|
18
|
+
example_id = generate_id
|
|
19
|
+
include_rel_param = context.params['include-relationships']
|
|
20
|
+
relationships = false if FALSEY_STRINGS.include?(include_rel_param)
|
|
8
21
|
return nil unless instance
|
|
9
|
-
resource_url = build_url(
|
|
22
|
+
resource_url = build_url(context, instance)
|
|
10
23
|
id = build_id(instance)
|
|
11
|
-
|
|
24
|
+
Structure::Objects::Resource.new.tap do |resource|
|
|
12
25
|
resource[:id] = id
|
|
13
26
|
resource[:type] = type
|
|
14
27
|
|
|
15
|
-
resource[:attributes]
|
|
16
|
-
|
|
28
|
+
resource[:attributes] = fields[type.to_sym].each_with_object(
|
|
29
|
+
Structure::Objects::Attributes.new
|
|
30
|
+
) do |member, attributes|
|
|
31
|
+
attribute = self.attributes.find { |a| a.name == member.to_sym }
|
|
32
|
+
unless attribute.supports_read_for_action?(context.action_name, context)
|
|
33
|
+
error_block =
|
|
34
|
+
context.resource.class.error_definitions[:internal_server_error]
|
|
35
|
+
context.errors.evaluate(
|
|
36
|
+
name,
|
|
37
|
+
error_block: error_block,
|
|
38
|
+
runtime_block: proc {}
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
attributes[member.to_sym] = attribute.resolve(
|
|
42
|
+
instance, context, example_id: example_id
|
|
43
|
+
)
|
|
17
44
|
end
|
|
18
45
|
|
|
19
|
-
resource[:links]
|
|
46
|
+
resource[:links] = Structure::Objects::Links.new(
|
|
20
47
|
self: resource_url
|
|
21
48
|
) if links
|
|
22
49
|
|
|
23
50
|
resource[:meta] = {
|
|
24
|
-
cursor: build_cursor_from_instance(
|
|
25
|
-
}
|
|
51
|
+
cursor: build_cursor_from_instance(context, instance)
|
|
52
|
+
} if include_cursor
|
|
53
|
+
|
|
54
|
+
resource[:relationships] = relationship_definitions.each_with_object(
|
|
55
|
+
Structure::Maps::Relationships.new
|
|
56
|
+
) do |rel, hash|
|
|
57
|
+
hash[rel.name] = build_relationship(context, instance, rel.name)
|
|
58
|
+
end if relationships || context.includes.present?
|
|
26
59
|
|
|
27
|
-
resource
|
|
28
|
-
hash[rel.name] = build_relationship(request, instance, rel.name)
|
|
29
|
-
end if relationships
|
|
60
|
+
block.call(resource, instance) if block
|
|
30
61
|
end
|
|
31
62
|
end
|
|
32
63
|
|
|
33
64
|
def build_resource_identifier(instance)
|
|
34
|
-
|
|
65
|
+
Structure::Objects::ResourceIdentifier.new(
|
|
35
66
|
id: build_id(instance),
|
|
36
67
|
type: type.to_s
|
|
37
68
|
)
|
|
38
69
|
end
|
|
39
70
|
|
|
40
|
-
def build_collection(
|
|
41
|
-
|
|
42
|
-
collection
|
|
43
|
-
|
|
71
|
+
def build_collection(
|
|
72
|
+
context,
|
|
73
|
+
collection,
|
|
74
|
+
fields:,
|
|
75
|
+
include_cursors: false,
|
|
76
|
+
&block
|
|
77
|
+
)
|
|
78
|
+
include_rel_param = context.params['include-relationships']
|
|
79
|
+
relationships = TRUTHY_STRINGS.include? include_rel_param
|
|
80
|
+
collection.each_with_object(
|
|
81
|
+
Structure::Collections::Resources.new
|
|
82
|
+
) do |instance, resources|
|
|
83
|
+
resources << build_resource(
|
|
84
|
+
context,
|
|
85
|
+
instance,
|
|
86
|
+
fields: fields,
|
|
87
|
+
relationships: relationships,
|
|
88
|
+
include_cursor: include_cursors,
|
|
89
|
+
&block
|
|
90
|
+
)
|
|
44
91
|
end
|
|
45
92
|
end
|
|
46
93
|
|
|
47
|
-
def build_cursor_from_instance(
|
|
48
|
-
sort_string =
|
|
94
|
+
def build_cursor_from_instance(context, instance)
|
|
95
|
+
sort_string = context.params['sort']
|
|
49
96
|
sort_fields = sort_fields_from_sort_string(sort_string)
|
|
50
97
|
attrs_with_values = sort_fields.each_with_object({}) do |field, hash|
|
|
51
98
|
hash[field.name] = instance.send(field.name)
|
|
@@ -60,23 +107,25 @@ module JSONAPIonify::Api
|
|
|
60
107
|
end
|
|
61
108
|
|
|
62
109
|
def build_identifier_collection(collection)
|
|
63
|
-
collection.each_with_object(
|
|
110
|
+
collection.each_with_object(
|
|
111
|
+
JSONAPIonify::Structure::Collections::ResourceIdentifiers.new
|
|
112
|
+
) do |instance, resource_identifiers|
|
|
64
113
|
resource_identifiers << build_resource_identifier(instance)
|
|
65
114
|
end
|
|
66
115
|
end
|
|
67
116
|
|
|
68
|
-
def build_relationship(
|
|
69
|
-
resource_url = build_url(
|
|
117
|
+
def build_relationship(context, instance, name, links: true, data: false)
|
|
118
|
+
resource_url = build_url(context, instance)
|
|
70
119
|
relationship = self.relationship(name)
|
|
71
120
|
JSONAPIonify::Structure::Objects::Relationship.new.tap do |rel|
|
|
72
121
|
rel[:links] = relationship.build_links(resource_url) if links
|
|
73
|
-
if data
|
|
122
|
+
if data || context.includes.present?
|
|
74
123
|
rel[:data] =
|
|
75
|
-
if relationship
|
|
124
|
+
if relationship.rel.is_a? Relationship::Many
|
|
76
125
|
instance.send(name).map do |child|
|
|
77
126
|
relationship.build_resource_identifier(child)
|
|
78
127
|
end
|
|
79
|
-
elsif relationship
|
|
128
|
+
elsif relationship.rel.is_a? Relationship::One
|
|
80
129
|
value = instance.send(name)
|
|
81
130
|
relationship.build_resource_identifier value if value
|
|
82
131
|
end
|
|
@@ -84,15 +133,15 @@ module JSONAPIonify::Api
|
|
|
84
133
|
end
|
|
85
134
|
end
|
|
86
135
|
|
|
87
|
-
def build_url(
|
|
88
|
-
URI.parse(request.root_url).tap do |uri|
|
|
136
|
+
def build_url(context, instance = nil)
|
|
137
|
+
URI.parse(context.request.root_url).tap do |uri|
|
|
89
138
|
uri.path =
|
|
90
139
|
if instance
|
|
91
140
|
File.join(uri.path, type, build_id(instance))
|
|
92
141
|
else
|
|
93
|
-
File.join(request.root_url, type)
|
|
142
|
+
File.join(context.request.root_url, type)
|
|
94
143
|
end
|
|
95
|
-
sticky_params = self.sticky_params(
|
|
144
|
+
sticky_params = self.sticky_params(context.params)
|
|
96
145
|
uri.query = sticky_params.to_param if sticky_params.present?
|
|
97
146
|
end.to_s
|
|
98
147
|
end
|
|
@@ -103,7 +152,9 @@ module JSONAPIonify::Api
|
|
|
103
152
|
end
|
|
104
153
|
|
|
105
154
|
included do
|
|
106
|
-
|
|
155
|
+
delegated_methods = ClassMethods.instance_methods -
|
|
156
|
+
JSONAPIonify::Api::Resource::Builders.instance_methods
|
|
157
|
+
delegate(*delegated_methods, to: :class)
|
|
107
158
|
end
|
|
108
159
|
|
|
109
160
|
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Caching
|
|
3
|
+
def cache key, **options
|
|
4
|
+
raise Errors::DoubleCacheError, "Cache was already called for this action" if @cache_called
|
|
5
|
+
@cache_called = true
|
|
6
|
+
@cache_options.merge! options
|
|
7
|
+
|
|
8
|
+
# Build the cache key, and obscure it.
|
|
9
|
+
@__context.meta[:cache_key] = @cache_options[:key] = cache_key(
|
|
10
|
+
path: @__context.request.path,
|
|
11
|
+
accept: @__context.request.accept,
|
|
12
|
+
params: @__context.params,
|
|
13
|
+
key: key
|
|
14
|
+
)
|
|
15
|
+
# If the cache exists, then fail to cache miss
|
|
16
|
+
if self.class.cache_store.exist?(@cache_options[:key]) && !@__context.invalidate_cache?
|
|
17
|
+
raise Errors::CacheHit, @cache_options[:key]
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cache_key(**options)
|
|
22
|
+
self.class.cache_key(
|
|
23
|
+
**options,
|
|
24
|
+
action_name: action_name
|
|
25
|
+
)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
module Resource::Caller
|
|
3
|
+
def call
|
|
4
|
+
do_respond = proc { __respond }
|
|
5
|
+
do_request = proc { __request }
|
|
6
|
+
response = @callbacks ? run_callbacks(:request, @__context, &do_request) : do_request.call
|
|
7
|
+
rescue Errors::RequestError => e
|
|
8
|
+
raise e unless errors.present?
|
|
9
|
+
response = error_response
|
|
10
|
+
rescue Errors::CacheHit
|
|
11
|
+
JSONAPIonify.logger.info "Cache Hit: #{@cache_options[:key]}"
|
|
12
|
+
response = self.class.cache_store.read @cache_options[:key]
|
|
13
|
+
rescue Exception => exception
|
|
14
|
+
response = rescued_response exception, @__context, do_respond
|
|
15
|
+
ensure
|
|
16
|
+
self.class.cache_store.delete @cache_options[:key] unless response[0] < 300
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def response_definition
|
|
20
|
+
action.responses.find { |response| response.accept_with_matcher? @__context } ||
|
|
21
|
+
action.responses.find { |response| response.accept_with_header? @__context } ||
|
|
22
|
+
error_now(:not_acceptable)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
private
|
|
26
|
+
|
|
27
|
+
def __commit
|
|
28
|
+
instance_exec(@__context, &action.block)
|
|
29
|
+
halt if errors.present?
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def __commit_and_respond
|
|
33
|
+
do_respond = proc { __respond }
|
|
34
|
+
do_commit = proc { __commit }
|
|
35
|
+
halt if errors.present?
|
|
36
|
+
action.name && @callbacks ? run_callbacks("commit_#{action.name}", @__context, &do_commit) : do_commit.call
|
|
37
|
+
@callbacks ? run_callbacks(:response, @__context, &do_respond) : do_respond.call
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def __request
|
|
41
|
+
do_commit_and_respond = proc { __commit_and_respond }
|
|
42
|
+
action.name && @callbacks ? run_callbacks(action.name, @__context, &do_commit_and_respond) : do_commit_and_respond.call
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def __respond(**options)
|
|
46
|
+
raise Errors::DoubleRespondError if @response_called
|
|
47
|
+
@response_called = true
|
|
48
|
+
response_definition.call(self, @__context, **options).tap do |status, headers, body|
|
|
49
|
+
halt if errors.present?
|
|
50
|
+
if response_definition.cacheable && @cache_options.present?
|
|
51
|
+
JSONAPIonify.logger.info "Cache Miss: #{@cache_options[:key]}"
|
|
52
|
+
self.class.cache_store.write(
|
|
53
|
+
@cache_options[:key],
|
|
54
|
+
[status, headers, body.body],
|
|
55
|
+
**@cache_options.except(:key)
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|