jsonapionify 0.11.11 → 0.12.0

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 (46) hide show
  1. checksums.yaml +8 -8
  2. data/README.md +3 -0
  3. data/examples/example_api.rb +80 -0
  4. data/jsonapionify.gemspec +1 -0
  5. data/lib/jsonapionify/api/action/documentation.rb +2 -2
  6. data/lib/jsonapionify/api/attribute.rb +3 -2
  7. data/lib/jsonapionify/api/context.rb +8 -3
  8. data/lib/jsonapionify/api/context_delegate.rb +6 -0
  9. data/lib/jsonapionify/api/relationship.rb +5 -10
  10. data/lib/jsonapionify/api/relationship/many.rb +46 -14
  11. data/lib/jsonapionify/api/relationship/one.rb +31 -8
  12. data/lib/jsonapionify/api/resource.rb +3 -6
  13. data/lib/jsonapionify/api/resource/builders/fields_builder.rb +2 -2
  14. data/lib/jsonapionify/api/resource/builders/relationship_builder.rb +3 -2
  15. data/lib/jsonapionify/api/resource/builders/relationships_builder.rb +3 -2
  16. data/lib/jsonapionify/api/resource/caching.rb +1 -4
  17. data/lib/jsonapionify/api/resource/callbacks.rb +60 -0
  18. data/lib/jsonapionify/api/resource/caller.rb +8 -7
  19. data/lib/jsonapionify/api/resource/defaults/actions.rb +15 -12
  20. data/lib/jsonapionify/api/resource/defaults/hooks.rb +10 -57
  21. data/lib/jsonapionify/api/resource/defaults/options.rb +3 -8
  22. data/lib/jsonapionify/api/resource/defaults/params.rb +0 -2
  23. data/lib/jsonapionify/api/resource/defaults/request_contexts.rb +24 -29
  24. data/lib/jsonapionify/api/resource/defaults/response_contexts.rb +11 -18
  25. data/lib/jsonapionify/api/resource/definitions/actions.rb +38 -22
  26. data/lib/jsonapionify/api/resource/definitions/attributes.rb +2 -2
  27. data/lib/jsonapionify/api/resource/definitions/helpers.rb +1 -1
  28. data/lib/jsonapionify/api/resource/definitions/includes.rb +16 -0
  29. data/lib/jsonapionify/api/resource/definitions/pagination.rb +7 -11
  30. data/lib/jsonapionify/api/resource/definitions/params.rb +19 -26
  31. data/lib/jsonapionify/api/resource/definitions/relationships.rb +3 -3
  32. data/lib/jsonapionify/api/resource/definitions/request_headers.rb +14 -16
  33. data/lib/jsonapionify/api/resource/definitions/response_headers.rb +2 -1
  34. data/lib/jsonapionify/api/resource/definitions/scopes.rb +17 -6
  35. data/lib/jsonapionify/api/resource/definitions/sorting.rb +8 -7
  36. data/lib/jsonapionify/api/resource/error_handling.rb +3 -2
  37. data/lib/jsonapionify/api/resource/exec.rb +3 -1
  38. data/lib/jsonapionify/api/resource/includer.rb +20 -51
  39. data/lib/jsonapionify/api/response.rb +3 -1
  40. data/lib/jsonapionify/callbacks.rb +10 -7
  41. data/lib/jsonapionify/destructured_proc.rb +27 -0
  42. data/lib/jsonapionify/structure/collections/base.rb +20 -8
  43. data/lib/jsonapionify/structure/objects/base.rb +11 -2
  44. data/lib/jsonapionify/structure/objects/resource_identifier.rb +9 -14
  45. data/lib/jsonapionify/version.rb +1 -1
  46. metadata +20 -2
@@ -14,11 +14,11 @@ module JSONAPIonify::Api
14
14
  end
15
15
 
16
16
  def resource_fields
17
- fields[resource_type] || {}
17
+ fields && fields[resource_type]
18
18
  end
19
19
 
20
20
  def build
21
- fields.nil? ? build_default : build_sparce
21
+ resource_fields.nil? ? build_default : build_sparce
22
22
  end
23
23
 
24
24
  end
@@ -25,13 +25,14 @@ module JSONAPIonify::Api
25
25
  private
26
26
 
27
27
  def build_data
28
+ rel_resource = resource.relationship(relationship.name)
28
29
  case relationship
29
30
  when Relationship::Many
30
31
  resolution.map do |child|
31
- relationship.build_resource_identifier(child)
32
+ rel_resource.build_resource_identifier(instance: child)
32
33
  end
33
34
  when Relationship::One
34
- relationship.build_resource_identifier resolution
35
+ rel_resource.build_resource_identifier instance: resolution
35
36
  end
36
37
  end
37
38
 
@@ -3,19 +3,20 @@ module JSONAPIonify::Api
3
3
  class RelationshipsBuilder < FieldsBuilder
4
4
 
5
5
  delegate :relationships, to: :resource, prefix: true
6
+ delegate :includes, to: :context
6
7
 
7
8
  private
8
9
 
9
10
  def build_default
10
11
  resource_relationships.each_with_object(Objects::Relationships.new) do |relationship, attrs|
11
- unless relationship.hidden_for_action?(action_name)
12
+ if !relationship.hidden_for_action?(action_name) || includes.keys.include?(relationship.name.to_s)
12
13
  attrs[relationship.name] = build_relationship(relationship)
13
14
  end
14
15
  end
15
16
  end
16
17
 
17
18
  def build_sparce
18
- resource_fields.each_with_object(Objects::Relationships.new) do |field, attrs|
19
+ (resource_fields + includes.keys).each_with_object(Objects::Relationships.new) do |field, attrs|
19
20
  field = field.to_sym
20
21
  relationship = resource_relationships.find { |rel| rel.name == field }
21
22
  attrs[field] = build_relationship(relationship) if relationship
@@ -19,10 +19,7 @@ module JSONAPIonify::Api
19
19
  end
20
20
 
21
21
  def cache_key(**options)
22
- self.class.cache_key(
23
- **options,
24
- action_name: action_name
25
- )
22
+ self.class.cache_key(**options, action_name: @__context.action_name)
26
23
  end
27
24
  end
28
25
  end
@@ -0,0 +1,60 @@
1
+ require 'active_support/concern'
2
+
3
+ module JSONAPIonify::Api
4
+ module Resource::Callbacks
5
+ extend ActiveSupport::Concern
6
+ using JSONAPIonify::DestructuredProc
7
+ included do
8
+
9
+ def self.define_callbacks(*names)
10
+ names.each do |name|
11
+ chains = {
12
+ main: "__#{name}_callback_chain",
13
+ before: "__#{name}_before_callback_chain",
14
+ after: "__#{name}_after_callback_chain"
15
+ }
16
+ define_method chains[:main] do |*args, &block|
17
+ block ||= proc {}
18
+ if send(chains[:before], *args) != false
19
+ value = begin
20
+ instance_exec(*args, @__context, &block&.destructure)
21
+ rescue => e
22
+ e.backtrace.unshift block.source_location.join(':') + ":in `(callback)`"
23
+ raise e
24
+ end
25
+ value if send(chains[:after], *args) != false
26
+ end
27
+ end unless method_defined? chains[:main]
28
+
29
+ # Define before and after chains
30
+ %i{after before}.each do |timing|
31
+ define_method chains[timing] { |*| } unless method_defined? chains[timing]
32
+ callback_name = "#{timing}_#{name}"
33
+ define_singleton_method callback_name do |sym = nil, &outer_block|
34
+ outer_block = (outer_block || sym).to_proc
35
+ prev_chain = instance_method(chains[timing])
36
+ define_method chains[timing] do |*args, &block|
37
+ begin
38
+ if prev_chain.bind(self).call(*args, @__context, &block&.destructure) != false
39
+ instance_exec(*args, @__context, &outer_block&.destructure)
40
+ end
41
+ rescue => e
42
+ e.backtrace.unshift outer_block.source_location.join(':') + ":in `(callback) #{callback_name}`"
43
+ raise e
44
+ end
45
+ end
46
+ end
47
+ end
48
+
49
+ private(*chains.values)
50
+
51
+ end
52
+ end
53
+ end
54
+
55
+ def run_callbacks(name, *args, &block)
56
+ send("__#{name}_callback_chain", *args, &block)
57
+ end
58
+
59
+ end
60
+ end
@@ -1,14 +1,16 @@
1
1
  module JSONAPIonify::Api
2
2
  module Resource::Caller
3
+ using JSONAPIonify::DestructuredProc
4
+
3
5
  def call
4
6
  do_respond = proc { __respond }
5
7
  do_request = proc { __request }
6
- response = @callbacks ? run_callbacks(:request, @__context, &do_request) : do_request.call
8
+ response = @callbacks ? run_callbacks(:request, &do_request) : do_request.call
7
9
  rescue Errors::RequestError => e
8
10
  raise e unless errors.present?
9
11
  response = error_response
10
12
  rescue Errors::CacheHit
11
- JSONAPIonify.logger.info "Cache Hit: #{@cache_options[:key]}"
13
+ JSONAPIonify.logger.debug "Cache Hit: #{@cache_options[:key]}"
12
14
  response = self.class.cache_store.read @cache_options[:key]
13
15
  rescue Exception => exception
14
16
  response = rescued_response exception, @__context, do_respond
@@ -25,21 +27,20 @@ module JSONAPIonify::Api
25
27
  private
26
28
 
27
29
  def __commit
28
- instance_exec(@__context, &action.block)
29
- halt if errors.present?
30
+ instance_exec(@__context, &action.block.destructure)
30
31
  end
31
32
 
32
33
  def __commit_and_respond
33
34
  do_respond = proc { __respond }
34
35
  do_commit = proc { __commit }
35
36
  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
37
+ action.name && @callbacks ? run_callbacks("commit_#{action.name}", &do_commit) : do_commit.call
38
+ @callbacks ? run_callbacks(:response, &do_respond) : do_respond.call
38
39
  end
39
40
 
40
41
  def __request
41
42
  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
+ action.name && @callbacks ? run_callbacks(action.name, &do_commit_and_respond) : do_commit_and_respond.call
43
44
  end
44
45
 
45
46
  def __respond(**options)
@@ -3,20 +3,21 @@ module JSONAPIonify::Api
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- context :path_actions, readonly: true, persisted: true do |context|
7
- self.class.path_actions(context.request)
6
+ context :path_actions, readonly: true, persisted: true do |request:|
7
+ self.class.path_actions(request)
8
8
  end
9
9
 
10
- context :http_allow, readonly: true, persisted: true do |context|
11
- context.path_actions.map(&:request_method)
10
+ context :http_allow, readonly: true, persisted: true do |path_actions:|
11
+ path_actions.map(&:request_method)
12
12
  end
13
13
 
14
- context(:action_name, persisted: true) {}
14
+ context(:action_name, persisted: true)
15
+
15
16
  define_action(:options, 'OPTIONS', '*', cacheable: true, callbacks: false) do
16
17
  cache 'options-request'
17
- end.response(status: 200) do |context|
18
- response_headers['Allow'] = context.http_allow.join(', ')
19
- requests = context.path_actions.each_with_object({}) do |action, h|
18
+ end.response(status: 200) do |context, request:, http_allow:, path_actions:|
19
+ response_headers['Allow'] = http_allow.join(', ')
20
+ requests = path_actions.each_with_object({}) do |action, h|
20
21
  request_attributes_json = attributes.select do |attr|
21
22
  attr.supports_write_for_action? action.name, context
22
23
  end.map do |attr|
@@ -26,11 +27,11 @@ module JSONAPIonify::Api
26
27
  response_attributes_json = attributes.select do |attr|
27
28
  attr.supports_read_for_action? action.name, context
28
29
  end.map do |attr|
29
- attr.options_json_for_action(action.name, context)
30
+ attr.options_json_for_action action.name, context
30
31
  end
31
32
 
32
- h['path'] = context.request.path
33
- h['url'] = context.request.url
33
+ h['path'] = request.path
34
+ h['url'] = request.url
34
35
  action_options = h[action.request_method] = {}
35
36
 
36
37
  if ['GET', 'POST', 'PUT', 'PATCH'].include? action.request_method
@@ -50,7 +51,9 @@ module JSONAPIonify::Api
50
51
  ).to_json
51
52
  end
52
53
 
53
- before(:create) { |context| context.instance = context.new_instance }
54
+ before(:create) do |context|
55
+ context.instance = context.new_instance
56
+ end
54
57
  read
55
58
  end
56
59
  end
@@ -3,66 +3,19 @@ module JSONAPIonify::Api
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- before :commit_create, :commit_update do |context|
7
- # Assign the attributes
8
- context.request_attributes.each do |key, value|
9
- context.instance.send "#{key}=", value
10
- end
11
-
12
- # Assign the relationships
13
- context.request_relationships.each do |key, value|
14
- context.instance.send "#{key}=", value
15
- end
16
-
17
- end
18
-
19
- after :commit_create, :commit_update do |context|
20
- try_commit(context.instance)
21
- end
22
-
23
- after :commit_delete do |context|
24
- if defined?(ActiveRecord) && context.instance.is_a?(ActiveRecord::Base)
25
- context.instance.destroy
26
- end
27
- end
28
-
29
- before :commit_add do |context|
30
- context.scope.concat context.request_instances
31
- end
32
-
33
- before :commit_remove do |context|
34
- context.request_instances.each { |instance| context.scope.delete(instance) }
35
- end
36
-
37
- before :commit_replace do |context|
38
- case self.class.rel
39
- when Relationship::One
40
- context.owner.send "#{self.class.rel.name}=", context.request_instance
41
- try_commit(context.owner)
42
- when Relationship::Many
43
- instances_to_add = context.request_instances - context.scope
44
- instances_to_delete = context.scope - context.request_instances
45
- instances_to_delete.each { |instance| context.scope.delete(instance) }
46
- context.scope.append instances_to_add
47
- end
48
- end
49
- end
50
-
51
- def try_commit(instance)
52
- if defined?(ActiveRecord) && instance.is_a?(ActiveRecord::Base)
53
- commit_active_record(instance)
54
- end
55
- end
56
-
57
- def commit_active_record(instance)
58
- instance.save
59
- if instance.errors.present?
60
- instance.errors.messages.each do |attr, messages|
61
- messages.each do |message|
62
- error :invalid_attribute, attr, message
6
+ after :commit_update, :commit_create do |instance:|
7
+ if defined?(ActiveRecord) && instance.is_a?(ActiveRecord::Base)
8
+ # Collect Errors
9
+ if instance.errors.present?
10
+ instance.errors.messages.each do |attr, messages|
11
+ messages.each do |message|
12
+ error :invalid_attribute, attr, message
13
+ end
14
+ end
63
15
  end
64
16
  end
65
17
  end
66
18
  end
19
+
67
20
  end
68
21
  end
@@ -11,7 +11,7 @@ module JSONAPIonify::Api
11
11
  included do
12
12
  id :id
13
13
  scope { self.type.classify.constantize }
14
- collection do |scope, context|
14
+ collection do |scope|
15
15
  Resource::Defaults::Options.scope_is_active_record?(scope) ? scope.all : scope
16
16
  end
17
17
 
@@ -25,13 +25,8 @@ module JSONAPIonify::Api
25
25
  scope.new
26
26
  end
27
27
 
28
- before do |context|
29
- context.request_headers # pull request_headers so they verify
30
- end
31
-
32
- before do |context|
33
- context.params # pull params so they verify
34
- end
28
+ # Invoke validating contexts
29
+ before { |request_headers:| }
35
30
 
36
31
  end
37
32
  end
@@ -3,8 +3,6 @@ module JSONAPIonify::Api
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- param :'include-relationships'
7
-
8
6
  # Configure the default sort
9
7
  default_sort 'id'
10
8
  end
@@ -3,37 +3,35 @@ module JSONAPIonify::Api
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
-
7
- context(:request_body, readonly: true, persisted: true) do |context|
8
- context.request.body.read
6
+ context(:request_body, readonly: true, persisted: true) do |request:|
7
+ request.body.read
9
8
  end
10
9
 
11
- context(:request_object, readonly: true, persisted: true) do |context|
12
- JSONAPIonify.parse(context.request_body).as(:client).tap do |input|
10
+ context(:request_object, readonly: true, persisted: true) do |context, request_body:|
11
+ JSONAPIonify.parse(request_body).as(:client).tap do |input|
13
12
  error_now(:request_object_invalid, context, input) unless input.validate
14
13
  end
15
14
  end
16
15
 
17
- context(:id, readonly: true, persisted: true) do |context|
18
- context.request.env['jsonapionify.id']
16
+ context(:id, readonly: true, persisted: true) do |request:|
17
+ request.env['jsonapionify.id']
19
18
  end
20
19
 
21
- context(:request_id, readonly: true, persisted: true) do |context|
22
- context.request_data[:id]
20
+ context(:request_id, readonly: true, persisted: true) do |request_data:|
21
+ request_data[:id]
23
22
  end
24
23
 
25
- context(:request_attributes, readonly: true, persisted: true) do |context|
24
+ context(:request_attributes, readonly: true, persisted: true) do |context, request:, action_name:, id:, request_data:|
26
25
  should_error = false
27
26
 
28
- request_attributes =
29
- context.request_data.fetch(:attributes) do
30
- error_now :attributes_missing
31
- end
27
+ request_attributes = request_data.fetch(:attributes) do
28
+ error_now :attributes_missing
29
+ end
32
30
 
33
31
  # Check for required attributes
34
32
  self.attributes.each do |attr|
35
33
  next unless attr.required_for_action?(action_name, context)
36
- if attr.read? || context.id
34
+ if attr.read? || id
37
35
  example_id = self.build_id(instance: context.instance)
38
36
  next unless attr.resolve(
39
37
  context.instance, context, example_id: example_id
@@ -64,10 +62,9 @@ module JSONAPIonify::Api
64
62
  end
65
63
  end
66
64
 
67
- context(:request_relationships, readonly: true, persisted: true) do |context|
68
- data = context.request_data
69
- if data[:relationships]
70
- data[:relationships].each_with_object({}) do |(name, rel), obj|
65
+ context(:request_relationships, readonly: true, persisted: true) do |request_data:|
66
+ if request_data[:relationships]
67
+ request_data[:relationships].each_with_object({}) do |(name, rel), obj|
71
68
  pointer = "data/relationships/#{name}/data"
72
69
  case rel[:data]
73
70
  when JSONAPIonify::Structure::Collections::Base
@@ -81,22 +78,20 @@ module JSONAPIonify::Api
81
78
  end
82
79
  end
83
80
 
84
- context(:request_instances, readonly: true, persisted: true) do |context|
85
- data = context.request_data
86
- (data ? find_instances(data, pointer: '/data') : [])
81
+ context(:request_instances, readonly: true, persisted: true) do |request_data:|
82
+ (request_data ? find_instances(request_data, pointer: '/data') : [])
87
83
  end
88
84
 
89
- context(:request_instance, readonly: true, persisted: true) do |context|
90
- find_instance(context.request_data, pointer: 'data')
85
+ context(:request_instance, readonly: true, persisted: true) do |request_data:|
86
+ find_instance(request_data, pointer: 'data')
91
87
  end
92
88
 
93
- context(:request_resource, readonly: true, persisted: true) do |context|
94
- item = context.request_data
95
- find_resource item, pointer: 'data'
89
+ context(:request_resource, readonly: true, persisted: true) do |request_data:|
90
+ find_resource request_data, pointer: 'data'
96
91
  end
97
92
 
98
- context(:request_data, readonly: true, persisted: true) do |context|
99
- context.request_object.fetch(:data) {
93
+ context(:request_data, readonly: true, persisted: true) do |request_object:|
94
+ request_object.fetch(:data) {
100
95
  error_now(:data_missing)
101
96
  }
102
97
  end
@@ -3,32 +3,25 @@ module JSONAPIonify::Api
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- context(:invalidate_cache?, readonly: true, persisted: true) { |c| c.includes.present? }
6
+ context(:invalidate_cache?, readonly: true, persisted: true) do |includes:|
7
+ includes.present?
8
+ end
7
9
 
8
10
  # Response Objects
9
- context(:links, readonly: true, persisted: true) do |context|
10
- context.response_object[:links]
11
+ context(:links, readonly: true, persisted: true) do |response_object:|
12
+ response_object[:links]
11
13
  end
12
14
 
13
- context(:meta, readonly: true, persisted: true) do |context|
14
- JSONAPIonify::Structure::Helpers::MetaDelegate.new context.response_object
15
+ context(:meta, readonly: true, persisted: true) do |response_object:|
16
+ JSONAPIonify::Structure::Helpers::MetaDelegate.new response_object
15
17
  end
16
18
 
17
- context(:response_object, readonly: true, persisted: true) do |context|
18
- JSONAPIonify.parse(links: { self: context.request.url })
19
+ context(:response_object, readonly: true, persisted: true) do |request:|
20
+ JSONAPIonify.parse(links: { self: request.url })
19
21
  end
20
22
 
21
- context(:response_collection, readonly: true) do |context|
22
- if context.root_request?
23
- collections = %i{
24
- paginated_collection
25
- sorted_collection
26
- collection
27
- }
28
- context.public_send collections.find { |c| context.respond_to? c }
29
- else
30
- context.collection
31
- end
23
+ context(:response_collection, readonly: true) do |collection:, nested_request: false, paginated_collection: nil, sorted_collection: nil|
24
+ nested_request ? collection : paginated_collection || sorted_collection || collection
32
25
  end
33
26
 
34
27
  end