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.
Files changed (84) hide show
  1. checksums.yaml +13 -5
  2. data/.rubocop.yml +1 -0
  3. data/.ruby-version +1 -1
  4. data/.travis.yml +8 -0
  5. data/README.md +85 -3
  6. data/Rakefile +14 -0
  7. data/jsonapionify.gemspec +3 -0
  8. data/lib/jsonapionify/api/action.rb +84 -121
  9. data/lib/jsonapionify/api/attribute.rb +97 -20
  10. data/lib/jsonapionify/api/base/class_methods.rb +5 -4
  11. data/lib/jsonapionify/api/base/delegation.rb +20 -4
  12. data/lib/jsonapionify/api/base/doc_helper.rb +3 -3
  13. data/lib/jsonapionify/api/base/reloader.rb +1 -1
  14. data/lib/jsonapionify/api/base/resource_definitions.rb +28 -15
  15. data/lib/jsonapionify/api/base.rb +6 -0
  16. data/lib/jsonapionify/api/context.rb +18 -5
  17. data/lib/jsonapionify/api/context_delegate.rb +24 -7
  18. data/lib/jsonapionify/api/errors.rb +2 -0
  19. data/lib/jsonapionify/api/errors_object.rb +6 -5
  20. data/lib/jsonapionify/api/relationship/blocks.rb +1 -1
  21. data/lib/jsonapionify/api/relationship/many.rb +35 -11
  22. data/lib/jsonapionify/api/relationship/one.rb +17 -7
  23. data/lib/jsonapionify/api/relationship.rb +20 -6
  24. data/lib/jsonapionify/api/resource/builders.rb +81 -30
  25. data/lib/jsonapionify/api/resource/caching.rb +28 -0
  26. data/lib/jsonapionify/api/resource/caller.rb +61 -0
  27. data/lib/jsonapionify/api/resource/class_methods.rb +6 -2
  28. data/lib/jsonapionify/api/resource/defaults/actions.rb +47 -0
  29. data/lib/jsonapionify/api/resource/defaults/errors.rb +61 -15
  30. data/lib/jsonapionify/api/resource/defaults/hooks.rb +68 -0
  31. data/lib/jsonapionify/api/resource/defaults/options.rb +16 -28
  32. data/lib/jsonapionify/api/resource/defaults/params.rb +3 -0
  33. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +80 -32
  34. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +13 -6
  35. data/lib/jsonapionify/api/resource/defaults.rb +1 -1
  36. data/lib/jsonapionify/api/resource/definitions/actions.rb +81 -55
  37. data/lib/jsonapionify/api/resource/definitions/attributes.rb +46 -10
  38. data/lib/jsonapionify/api/resource/definitions/contexts.rb +6 -2
  39. data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
  40. data/lib/jsonapionify/api/resource/definitions/pagination.rb +47 -56
  41. data/lib/jsonapionify/api/resource/definitions/params.rb +11 -15
  42. data/lib/jsonapionify/api/resource/definitions/relationships.rb +43 -7
  43. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +6 -3
  44. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +1 -1
  45. data/lib/jsonapionify/api/resource/definitions/scopes.rb +5 -5
  46. data/lib/jsonapionify/api/resource/definitions/sorting.rb +12 -11
  47. data/lib/jsonapionify/api/resource/definitions.rb +1 -1
  48. data/lib/jsonapionify/api/resource/error_handling.rb +92 -20
  49. data/lib/jsonapionify/api/resource/exec.rb +11 -0
  50. data/lib/jsonapionify/api/resource/includer.rb +89 -1
  51. data/lib/jsonapionify/api/resource.rb +55 -8
  52. data/lib/jsonapionify/api/response.rb +43 -14
  53. data/lib/jsonapionify/api/server/media_type.rb +36 -0
  54. data/lib/jsonapionify/api/server/request.rb +25 -11
  55. data/lib/jsonapionify/api/server.rb +8 -4
  56. data/lib/jsonapionify/api/sort_field.rb +18 -0
  57. data/lib/jsonapionify/api/sort_field_set.rb +1 -1
  58. data/lib/jsonapionify/api/test_helper.rb +46 -0
  59. data/lib/jsonapionify/documentation/template.erb +2 -2
  60. data/lib/jsonapionify/documentation.rb +10 -0
  61. data/lib/jsonapionify/structure/collections/base.rb +10 -3
  62. data/lib/jsonapionify/structure/helpers/object_defaults.rb +5 -10
  63. data/lib/jsonapionify/structure/maps/relationships.rb +4 -0
  64. data/lib/jsonapionify/structure/objects/attributes.rb +4 -0
  65. data/lib/jsonapionify/structure/objects/base.rb +22 -9
  66. data/lib/jsonapionify/structure/objects/error.rb +2 -0
  67. data/lib/jsonapionify/structure/objects/jsonapi.rb +1 -0
  68. data/lib/jsonapionify/structure/objects/link.rb +1 -0
  69. data/lib/jsonapionify/structure/objects/relationship.rb +2 -0
  70. data/lib/jsonapionify/structure/objects/resource.rb +2 -0
  71. data/lib/jsonapionify/structure/objects/resource_identifier.rb +12 -4
  72. data/lib/jsonapionify/structure/objects/top_level.rb +4 -2
  73. data/lib/jsonapionify/types/array_type.rb +16 -11
  74. data/lib/jsonapionify/types/boolean_type.rb +9 -4
  75. data/lib/jsonapionify/types/date_string_type.rb +7 -10
  76. data/lib/jsonapionify/types/float_type.rb +13 -0
  77. data/lib/jsonapionify/types/integer_type.rb +12 -0
  78. data/lib/jsonapionify/types/object_type.rb +7 -2
  79. data/lib/jsonapionify/types/string_type.rb +12 -0
  80. data/lib/jsonapionify/types/time_string_type.rb +8 -10
  81. data/lib/jsonapionify/types.rb +43 -5
  82. data/lib/jsonapionify/version.rb +1 -1
  83. data/lib/jsonapionify.rb +36 -1
  84. 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 Errors::ResourceNotFound => e
21
- raise e if resources_loaded?
22
- load_resources
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
- const_name = name.to_s.camelcase + 'Resource'
38
- remove_const(const_name) if const_defined? const_name
39
- resource_definitions[name.to_sym] = block
40
- end
41
-
42
- def extend_resource(name, &block)
43
- old = resource_definitions[name.to_sym]
44
- resource_definitions[name.to_sym] = proc do
45
- [old, block].each { |b| class_eval(&b) }
46
- end
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 < Struct.new :block, :readonly
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
- block = self.block || proc {}
6
- instance.instance_exec(delegate, &block)
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
- !!readonly
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 method_missing(*args, &block)
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
- delegate = self
12
- @definitions = definitions
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
- memo[name] ||= context.call(instance, delegate)
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
- memo[name] = value
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:, backtrace: nil)
37
- backtrace ||= caller
38
- error = Structure::Objects::Error.new
39
- evaluator = Evaluator.new(error)
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
- unless ENV['RACK_ENV'] == 'production'
45
+ if JSONAPIonify.show_backtrace == true
45
46
  error[:meta] ||= {}
46
47
  error[:meta][:backtrace] = backtrace
47
48
  end
@@ -38,4 +38,4 @@ module JSONAPIonify::Api
38
38
  end
39
39
  end
40
40
  end
41
- end
41
+ end
@@ -8,17 +8,28 @@ module JSONAPIonify::Api
8
8
  undef_method :read
9
9
  end
10
10
 
11
- define_singleton_method(:show) do |**options, &block|
12
- options[:prepend] = 'relationships'
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 |**options, &block|
20
- options[:prepend] = 'relationships'
21
- define_action(:replace, 'PATCH', '', true, :resource_identifier, **options, &block).response status: 200 do |context|
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 |**options, &block|
30
- options[:prepend] = 'relationships'
31
- define_action(:add, 'POST', '', true, :resource_identifier, **options, &block).response status: 200 do |context|
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 |**options, &block|
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', '', true, :resource_identifier, **options, &block).response status: 200 do |context|
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.owner_context.instance.send(rel.name)
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 |**options, &block|
13
- options[:prepend] = 'relationships'
14
- define_action(:show, 'GET', '', nil, :resource_identifier, **options, &block).response status: 200 do |context|
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 |**options, &block|
21
- options[:prepend] = 'relationships'
22
- define_action(:replace, 'PATCH', '', nil, :resource_identifier, **options, &block).response status: 200 do |context|
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.owner_context.instance.send(rel.name)
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
- ContextDelegate.new(request, rel.owner.new, rel.owner.context_definitions)
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
- def build_resource(request, instance, fields: api.fields, relationships: true, links: true)
7
- relationships = false if JSONAPIonify::FALSEY_STRINGS.include? request.params['include-relationships']
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(request, instance)
22
+ resource_url = build_url(context, instance)
10
23
  id = build_id(instance)
11
- JSONAPIonify::Structure::Objects::Resource.new.tap do |resource|
24
+ Structure::Objects::Resource.new.tap do |resource|
12
25
  resource[:id] = id
13
26
  resource[:type] = type
14
27
 
15
- resource[:attributes] = fields[type.to_sym].each_with_object(JSONAPIonify::Structure::Objects::Attributes.new) do |member, attributes|
16
- attributes[member.to_sym] = instance.public_send(member)
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] = JSONAPIonify::Structure::Objects::Links.new(
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(request, 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[:relationships] = relationship_definitions.each_with_object(JSONAPIonify::Structure::Maps::Relationships.new) do |rel, hash|
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
- JSONAPIonify::Structure::Objects::ResourceIdentifier.new(
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(request, collection, fields: api.fields)
41
- relationships = JSONAPIonify::TRUTHY_STRINGS.include? request.params['include-relationships']
42
- collection.each_with_object(JSONAPIonify::Structure::Collections::Resources.new) do |instance, resources|
43
- resources << build_resource(request, instance, fields: fields, relationships: relationships)
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(request, instance)
48
- sort_string = request.params['sort']
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(JSONAPIonify::Structure::Collections::ResourceIdentifiers.new) do |instance, resource_identifiers|
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(request, instance, name, links: true, data: false)
69
- resource_url = build_url(request, instance)
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 < Resource::RelationshipToMany
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 < Resource::RelationshipToOne
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(request, instance = nil)
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(request.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
- delegate *(ClassMethods.instance_methods - JSONAPIonify::Api::Resource::Builders.instance_methods), to: :class
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