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
|
@@ -3,10 +3,10 @@ module JSONAPIonify::Api
|
|
|
3
3
|
|
|
4
4
|
def scope(&block)
|
|
5
5
|
define_singleton_method(:current_scope) do
|
|
6
|
-
|
|
6
|
+
instance_exec(OpenStruct.new, &block)
|
|
7
7
|
end
|
|
8
|
-
context :scope do
|
|
9
|
-
|
|
8
|
+
context :scope do |context|
|
|
9
|
+
instance_exec(context, &block)
|
|
10
10
|
end
|
|
11
11
|
end
|
|
12
12
|
|
|
@@ -16,7 +16,7 @@ module JSONAPIonify::Api
|
|
|
16
16
|
define_singleton_method(:find_instance) do |id|
|
|
17
17
|
instance_exec(current_scope, id, OpenStruct.new, &block)
|
|
18
18
|
end
|
|
19
|
-
context :instance do |context|
|
|
19
|
+
context :instance, persisted: true do |context|
|
|
20
20
|
instance_exec(context.scope, context.id, context, &block)
|
|
21
21
|
end
|
|
22
22
|
end
|
|
@@ -31,7 +31,7 @@ module JSONAPIonify::Api
|
|
|
31
31
|
define_singleton_method(:build_instance) do
|
|
32
32
|
Object.new.instance_exec(current_scope, &block)
|
|
33
33
|
end
|
|
34
|
-
context :new_instance do |context|
|
|
34
|
+
context :new_instance, persisted: true, readonly: true do |context|
|
|
35
35
|
Object.new.instance_exec(context.scope, context, &block)
|
|
36
36
|
end
|
|
37
37
|
end
|
|
@@ -10,28 +10,32 @@ module JSONAPIonify::Api
|
|
|
10
10
|
delegate :sort_fields_from_sort_string, to: :class
|
|
11
11
|
|
|
12
12
|
# Define Contexts
|
|
13
|
-
context :sorted_collection do |context|
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
context :sorted_collection, readonly: true do |context|
|
|
14
|
+
if context.root_request?
|
|
15
|
+
_, block = sorting_strategies.to_a.reverse.to_h.find do |mod, _|
|
|
16
|
+
Object.const_defined?(mod, false) && context.collection.class <= Object.const_get(mod, false)
|
|
17
|
+
end
|
|
18
|
+
context.reset(:sort_params)
|
|
19
|
+
instance_exec(context.collection, context.sort_params, context, &block)
|
|
20
|
+
else
|
|
21
|
+
context.collection
|
|
16
22
|
end
|
|
17
|
-
context.reset(:sort_params)
|
|
18
|
-
instance_exec(context.collection, context.sort_params, context, &block)
|
|
19
23
|
end
|
|
20
24
|
|
|
21
|
-
context(:sort_params, readonly: true) do |context|
|
|
25
|
+
context(:sort_params, readonly: true, persisted: true) do |context|
|
|
22
26
|
sort_fields_from_sort_string(context.params['sort']).tap do |fields|
|
|
23
27
|
should_error = false
|
|
24
28
|
fields.each do |field|
|
|
25
29
|
unless self.class.field_valid?(field.name) || field.name == id_attribute
|
|
26
30
|
should_error = true
|
|
27
|
-
type
|
|
31
|
+
type = self.class.type
|
|
28
32
|
error :sort_parameter_invalid do
|
|
29
33
|
detail "resource `#{type}` does not have field: #{field.name}"
|
|
30
34
|
end
|
|
31
35
|
next
|
|
32
36
|
end
|
|
33
37
|
end
|
|
34
|
-
|
|
38
|
+
halt if should_error
|
|
35
39
|
end
|
|
36
40
|
end
|
|
37
41
|
|
|
@@ -47,9 +51,6 @@ module JSONAPIonify::Api
|
|
|
47
51
|
collection.reorder(fields.to_hash).order(self.class.id_attribute)
|
|
48
52
|
end
|
|
49
53
|
|
|
50
|
-
# Configure the default sort
|
|
51
|
-
default_sort 'id'
|
|
52
|
-
|
|
53
54
|
end
|
|
54
55
|
end
|
|
55
56
|
|
|
@@ -1,12 +1,27 @@
|
|
|
1
|
+
require 'unstrict_proc'
|
|
2
|
+
|
|
1
3
|
module JSONAPIonify::Api
|
|
2
4
|
module Resource::ErrorHandling
|
|
3
5
|
extend ActiveSupport::Concern
|
|
6
|
+
using UnstrictProc
|
|
4
7
|
|
|
5
8
|
included do
|
|
6
9
|
include ActiveSupport::Rescuable
|
|
7
|
-
context(:errors, readonly: true) do
|
|
10
|
+
context(:errors, readonly: true, persisted: true) do
|
|
8
11
|
ErrorsObject.new
|
|
9
12
|
end
|
|
13
|
+
register_exception Exception, error: :internal_server_error do |exception|
|
|
14
|
+
if JSONAPIonify.verbose_errors
|
|
15
|
+
detail exception.message
|
|
16
|
+
meta[:error_class] = exception.class.name
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
register_exception Errors::RequestError, error: :internal_server_error do |exception|
|
|
20
|
+
if JSONAPIonify.verbose_errors
|
|
21
|
+
detail exception.message
|
|
22
|
+
meta[:error_class] = exception.class.name
|
|
23
|
+
end
|
|
24
|
+
end
|
|
10
25
|
end
|
|
11
26
|
|
|
12
27
|
module ClassMethods
|
|
@@ -15,11 +30,12 @@ module JSONAPIonify::Api
|
|
|
15
30
|
self.error_definitions = self.error_definitions.merge name.to_sym => block
|
|
16
31
|
end
|
|
17
32
|
|
|
18
|
-
def
|
|
19
|
-
|
|
33
|
+
def register_exception(*klasses, error:, &block)
|
|
34
|
+
block ||= proc {}
|
|
35
|
+
rescue_from(*klasses) do |exception, context|
|
|
20
36
|
errors.evaluate(
|
|
21
37
|
error_block: lookup_error(error),
|
|
22
|
-
runtime_block:
|
|
38
|
+
runtime_block: proc { instance_exec exception, context, &block },
|
|
23
39
|
backtrace: exception.backtrace
|
|
24
40
|
)
|
|
25
41
|
end
|
|
@@ -37,6 +53,35 @@ module JSONAPIonify::Api
|
|
|
37
53
|
@error_definitions
|
|
38
54
|
end
|
|
39
55
|
end
|
|
56
|
+
|
|
57
|
+
def sorted_rescue_handlers
|
|
58
|
+
handlers = self.rescue_handlers.dup
|
|
59
|
+
# logic to find invalid order
|
|
60
|
+
out_of_order_class = lambda do |klasses|
|
|
61
|
+
klass_index = nil
|
|
62
|
+
parent_index = nil
|
|
63
|
+
sort_class = klasses.find do |klass|
|
|
64
|
+
klass_index = klasses.find_index { |k| k == klass }
|
|
65
|
+
parent_index = klasses[0..klass_index].find_index { |k| k < klass }
|
|
66
|
+
end
|
|
67
|
+
sort_class ? [klass_index, parent_index] : nil
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
# Map handler classes
|
|
71
|
+
klasses = handlers.map do |klass_name, _|
|
|
72
|
+
klass = self.class.const_get(klass_name) rescue nil
|
|
73
|
+
klass ||= klass_name.constantize rescue nil
|
|
74
|
+
klass
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Loop until things are ordered
|
|
78
|
+
while (result = out_of_order_class[klasses])
|
|
79
|
+
klass_index, parent_index = result
|
|
80
|
+
handler = klasses.delete_at klass_index
|
|
81
|
+
handlers = [*handlers[0..parent_index-1], handler, *handlers[parent_index..-1]]
|
|
82
|
+
end
|
|
83
|
+
handlers.reverse
|
|
84
|
+
end
|
|
40
85
|
end
|
|
41
86
|
|
|
42
87
|
def error(name, *args, &block)
|
|
@@ -49,7 +94,7 @@ module JSONAPIonify::Api
|
|
|
49
94
|
|
|
50
95
|
def error_now(name, *args, &block)
|
|
51
96
|
error(name, *args, &block)
|
|
52
|
-
|
|
97
|
+
halt
|
|
53
98
|
end
|
|
54
99
|
|
|
55
100
|
def set_errors(collection)
|
|
@@ -60,6 +105,12 @@ module JSONAPIonify::Api
|
|
|
60
105
|
errors.meta
|
|
61
106
|
end
|
|
62
107
|
|
|
108
|
+
def halt
|
|
109
|
+
error = Errors::RequestError.new
|
|
110
|
+
error.set_backtrace caller
|
|
111
|
+
raise error
|
|
112
|
+
end
|
|
113
|
+
|
|
63
114
|
private
|
|
64
115
|
|
|
65
116
|
def lookup_error(name)
|
|
@@ -68,22 +119,43 @@ module JSONAPIonify::Api
|
|
|
68
119
|
end
|
|
69
120
|
end
|
|
70
121
|
|
|
71
|
-
def
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
122
|
+
def handler_for_rescue(exception)
|
|
123
|
+
_, rescuer = self.class.sorted_rescue_handlers.find do |klass_name, _|
|
|
124
|
+
klass = self.class.const_get(klass_name) rescue nil
|
|
125
|
+
klass ||= klass_name.constantize rescue nil
|
|
126
|
+
exception.is_a?(klass) if klass
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
case rescuer
|
|
130
|
+
when Symbol
|
|
131
|
+
method(rescuer)
|
|
132
|
+
when Proc
|
|
133
|
+
rescuer
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def invoke_rescue_handler(handler, exception, context, respond_proc)
|
|
138
|
+
status, headers, body =
|
|
139
|
+
instance_exec exception, context, respond_proc, &handler.unstrict
|
|
140
|
+
if status.is_a?(Fixnum) && headers.is_a?(Hash) && body.respond_to?(:each)
|
|
141
|
+
[status, headers, body]
|
|
142
|
+
else
|
|
143
|
+
error_response
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Tries to rescue the exception by looking up and calling a registered handler.
|
|
148
|
+
def rescue_with_handler(exception, context, respond_proc)
|
|
149
|
+
if (handler = handler_for_rescue(exception))
|
|
150
|
+
invoke_rescue_handler(handler, exception, context, respond_proc)
|
|
84
151
|
end
|
|
85
|
-
|
|
86
|
-
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def rescued_response(exception, context, respond_proc)
|
|
155
|
+
rescue_with_handler(exception, context, respond_proc)
|
|
156
|
+
rescue Exception => ex
|
|
157
|
+
handler = handler_for_rescue(Exception.new)
|
|
158
|
+
invoke_rescue_handler(handler, ex, context, respond_proc)
|
|
87
159
|
end
|
|
88
160
|
|
|
89
161
|
def error_response
|
|
@@ -1,9 +1,97 @@
|
|
|
1
|
+
require 'concurrent'
|
|
2
|
+
|
|
1
3
|
module JSONAPIonify::Api
|
|
2
4
|
module Resource::Includer
|
|
3
5
|
extend ActiveSupport::Concern
|
|
4
6
|
|
|
5
7
|
included do
|
|
6
|
-
|
|
8
|
+
before :list, :create, :read, :update do |context|
|
|
9
|
+
supports_includes = context.root_request? && context.includes.present?
|
|
10
|
+
is_active_record = defined?(ActiveRecord) && context.scope.respond_to?(:<) && context.scope < ActiveRecord::Base
|
|
11
|
+
if supports_includes && is_active_record
|
|
12
|
+
valid_includes = context.includes.select do |k, v|
|
|
13
|
+
context.scope._reflect_on_association(k)
|
|
14
|
+
end.to_h
|
|
15
|
+
context.scope = context.scope.includes valid_includes
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
after :commit_list do |context|
|
|
20
|
+
if context.includes.present?
|
|
21
|
+
included =
|
|
22
|
+
Concurrent::Array.new(context.response_collection).map do |instance|
|
|
23
|
+
fetch_included(context, owner: instance)
|
|
24
|
+
end.reduce(:+)
|
|
25
|
+
append_included(context, included)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
after :commit_create, :commit_read, :commit_update do |context|
|
|
30
|
+
if context.includes.present?
|
|
31
|
+
included = fetch_included(
|
|
32
|
+
context, owner: context.instance
|
|
33
|
+
)
|
|
34
|
+
append_included(context, included)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
context :root_request?, readonly: true do
|
|
39
|
+
true
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
context :includes do |context|
|
|
43
|
+
Resource::Includer.includes_to_hashes context.params['include']
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def append_included(context, included)
|
|
48
|
+
if included.present?
|
|
49
|
+
context.response_object[:included] = included
|
|
50
|
+
context.response_object[:included].tap(&:uniq!).reject! do |r|
|
|
51
|
+
Array.wrap(context.response_object[:included]).include?(r) ||
|
|
52
|
+
Array.wrap(context.response_object[:data]).include?(r)
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def fetch_included(context, **overrides)
|
|
58
|
+
collection = JSONAPIonify::Structure::Collections::IncludedResources.new
|
|
59
|
+
context.includes.each_with_object(collection) do |(name, _),|
|
|
60
|
+
res = self.class.relationship(name)
|
|
61
|
+
if res.rel.includable?
|
|
62
|
+
overrides = overrides.merge includes: context.includes[name],
|
|
63
|
+
errors: context.errors,
|
|
64
|
+
root_request?: false
|
|
65
|
+
*, body =
|
|
66
|
+
case res.rel
|
|
67
|
+
when Relationship::One
|
|
68
|
+
res.call_action(:read, context.request, **overrides)
|
|
69
|
+
when Relationship::Many
|
|
70
|
+
res.call_action(:list, context.request, **overrides)
|
|
71
|
+
end
|
|
72
|
+
collection.concat expand_body(body)
|
|
73
|
+
else
|
|
74
|
+
error :relationship_not_includable, res.rel.name
|
|
75
|
+
end
|
|
76
|
+
end.uniq
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def self.includes_to_hashes(path)
|
|
80
|
+
path.to_s.split(',').each_with_object({}) do |path, obj|
|
|
81
|
+
rel, *sub_path = path.split('.')
|
|
82
|
+
obj[rel] = includes_to_hashes(sub_path.join('.'))
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def expand_body(body)
|
|
87
|
+
case body
|
|
88
|
+
when Rack::BodyProxy
|
|
89
|
+
json = JSONAPIonify.parse(body.body.join)
|
|
90
|
+
Array.wrap(json[:data]) + (json[:included] || [])
|
|
91
|
+
when Array
|
|
92
|
+
json = JSONAPIonify.parse(body.join)
|
|
93
|
+
Array.wrap(json[:data]) + (json[:included] || [])
|
|
94
|
+
end
|
|
7
95
|
end
|
|
8
96
|
|
|
9
97
|
end
|
|
@@ -15,31 +15,78 @@ module JSONAPIonify::Api
|
|
|
15
15
|
include Builders
|
|
16
16
|
include Defaults
|
|
17
17
|
|
|
18
|
+
delegate :type, :attributes, :relationships, to: :class
|
|
19
|
+
|
|
18
20
|
def self.inherited(subclass)
|
|
19
21
|
super(subclass)
|
|
20
22
|
subclass.class_eval do
|
|
21
|
-
context(:api, readonly: true) { self.class.api }
|
|
22
|
-
context(:resource, readonly: true) { self }
|
|
23
|
+
context(:api, readonly: true, persisted: true) { self.class.api }
|
|
24
|
+
context(:resource, readonly: true, persisted: true) { self }
|
|
23
25
|
end
|
|
24
26
|
end
|
|
25
27
|
|
|
28
|
+
def self.cache_key(**options)
|
|
29
|
+
api.cache_key(
|
|
30
|
+
**options,
|
|
31
|
+
resource: name
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
26
35
|
def self.example_id_generator(&block)
|
|
27
|
-
|
|
36
|
+
index = 0
|
|
37
|
+
define_singleton_method(:generate_id) do
|
|
38
|
+
instance_exec index += 1, &block
|
|
39
|
+
end
|
|
40
|
+
context :example_id do
|
|
41
|
+
self.class.generate_id
|
|
42
|
+
end
|
|
28
43
|
end
|
|
29
44
|
|
|
30
|
-
def self.
|
|
31
|
-
id = generate_id
|
|
45
|
+
def self.example_instance_for_action(action, context)
|
|
46
|
+
id = generate_id
|
|
32
47
|
OpenStruct.new.tap do |instance|
|
|
33
|
-
instance.send "#{id_attribute}=",
|
|
34
|
-
attributes.select
|
|
35
|
-
|
|
48
|
+
instance.send "#{id_attribute}=", id.to_s
|
|
49
|
+
actionable_attributes = attributes.select do |attr|
|
|
50
|
+
attr.supports_read_for_action?(action, context)
|
|
51
|
+
end
|
|
52
|
+
actionable_attributes.each do |attribute|
|
|
53
|
+
instance.send "#{attribute.name}=", attribute.example(id)
|
|
36
54
|
end
|
|
37
55
|
end
|
|
38
56
|
end
|
|
39
57
|
|
|
40
58
|
example_id_generator { |val| val }
|
|
41
59
|
|
|
60
|
+
attr_reader :errors, :action, :response_headers
|
|
61
|
+
|
|
62
|
+
def initialize(
|
|
63
|
+
request:,
|
|
64
|
+
context_definitions: self.class.context_definitions,
|
|
65
|
+
commit: true,
|
|
66
|
+
callbacks: true,
|
|
67
|
+
context_overrides: {},
|
|
68
|
+
cacheable: true,
|
|
69
|
+
action: nil
|
|
70
|
+
)
|
|
71
|
+
context_overrides[:action_name] = action.name if action
|
|
72
|
+
@__context = ContextDelegate.new(
|
|
73
|
+
request,
|
|
74
|
+
self,
|
|
75
|
+
context_definitions,
|
|
76
|
+
context_overrides
|
|
77
|
+
)
|
|
78
|
+
@errors = @__context.errors
|
|
79
|
+
@action = action
|
|
80
|
+
@response_headers = @__context.response_headers
|
|
81
|
+
@callbacks = action ? callbacks : false
|
|
82
|
+
@cache_options = {}
|
|
83
|
+
extend Caller if commit && action
|
|
84
|
+
extend Exec unless action
|
|
85
|
+
extend Caching if cacheable
|
|
86
|
+
end
|
|
87
|
+
|
|
42
88
|
def action_name
|
|
89
|
+
action&.name
|
|
43
90
|
end
|
|
44
91
|
|
|
45
92
|
end
|
|
@@ -1,12 +1,20 @@
|
|
|
1
1
|
module JSONAPIonify::Api
|
|
2
2
|
class Response
|
|
3
|
-
attr_reader :action, :accept, :response_block, :status
|
|
3
|
+
attr_reader :action, :accept, :response_block, :status,
|
|
4
|
+
:matcher, :content_type
|
|
4
5
|
|
|
5
|
-
def initialize(action, accept: nil, status: nil, &block)
|
|
6
|
+
def initialize(action, accept: 'application/vnd.api+json', content_type: nil, status: nil, match: nil, cacheable: true, &block)
|
|
6
7
|
@action = action
|
|
7
8
|
@response_block = block || proc {}
|
|
8
|
-
@accept = accept
|
|
9
|
+
@accept = accept unless match
|
|
10
|
+
@content_type = content_type || (@accept == '*/*' ? nil : @accept)
|
|
11
|
+
@matcher = match || proc {}
|
|
9
12
|
@status = status || 200
|
|
13
|
+
@cacheable = cacheable
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def cacheable
|
|
17
|
+
action.cacheable && @cacheable
|
|
10
18
|
end
|
|
11
19
|
|
|
12
20
|
def ==(other)
|
|
@@ -16,12 +24,6 @@ module JSONAPIonify::Api
|
|
|
16
24
|
end
|
|
17
25
|
end
|
|
18
26
|
|
|
19
|
-
def accept?(request)
|
|
20
|
-
request.accept.any? do |accept|
|
|
21
|
-
@accept == accept || accept == '*/*' || self.accept == '*/*'
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
|
|
25
27
|
def documentation_object
|
|
26
28
|
OpenStruct.new(
|
|
27
29
|
accept: accept,
|
|
@@ -30,18 +32,45 @@ module JSONAPIonify::Api
|
|
|
30
32
|
)
|
|
31
33
|
end
|
|
32
34
|
|
|
33
|
-
def call(instance, context)
|
|
35
|
+
def call(instance, context, status: nil)
|
|
36
|
+
status ||= self.status
|
|
34
37
|
response = self
|
|
35
38
|
instance.instance_eval do
|
|
36
39
|
body = instance_exec(context, &response.response_block)
|
|
37
40
|
Rack::Response.new.tap do |rack_response|
|
|
38
|
-
rack_response.status =
|
|
39
|
-
response_headers.each
|
|
40
|
-
|
|
41
|
-
|
|
41
|
+
rack_response.status = status
|
|
42
|
+
response_headers.each do |k, v|
|
|
43
|
+
rack_response.headers[k.split('-').map(&:capitalize).join('-')] = v
|
|
44
|
+
end
|
|
45
|
+
rack_response.headers['Content-Type'] =
|
|
46
|
+
case response.content_type
|
|
47
|
+
when nil
|
|
48
|
+
raise(Errors::MissingContentType, 'missing content type')
|
|
49
|
+
when Proc
|
|
50
|
+
response.content_type.call(context)
|
|
51
|
+
else
|
|
52
|
+
response.content_type
|
|
53
|
+
end
|
|
54
|
+
if body.respond_to?(:each)
|
|
55
|
+
rack_response.body = body
|
|
56
|
+
elsif !body.nil?
|
|
57
|
+
rack_response.write(body)
|
|
58
|
+
end
|
|
42
59
|
end.finish
|
|
43
60
|
end
|
|
44
61
|
end
|
|
45
62
|
|
|
63
|
+
def accept_with_header?(context)
|
|
64
|
+
context.request.accept.any? do |accept|
|
|
65
|
+
self.accept == accept ||
|
|
66
|
+
(self.accept == '*/*' && !context.request.extension) ||
|
|
67
|
+
(accept == '*/*' && !context.request.extension)
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def accept_with_matcher?(context)
|
|
72
|
+
!!matcher.call(context)
|
|
73
|
+
end
|
|
74
|
+
|
|
46
75
|
end
|
|
47
76
|
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module JSONAPIonify::Api
|
|
2
|
+
class Server::MediaType
|
|
3
|
+
SPLIT_PATTERN = %r{\s*[;,]\s*}
|
|
4
|
+
|
|
5
|
+
class << self
|
|
6
|
+
# The media type (type/subtype) portion of the CONTENT_TYPE header
|
|
7
|
+
# without any media type parameters. e.g., when CONTENT_TYPE is
|
|
8
|
+
# "text/plain;charset=utf-8", the media-type is "text/plain".
|
|
9
|
+
#
|
|
10
|
+
# For more information on the use of media types in HTTP, see:
|
|
11
|
+
# http://www.w3.org/Protocols/rfc2616/rfc2616-sec3.html#sec3.7
|
|
12
|
+
def type(content_type)
|
|
13
|
+
return nil unless content_type
|
|
14
|
+
content_type.split(SPLIT_PATTERN, 2).first.downcase
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The media type parameters provided in CONTENT_TYPE as a Hash, or
|
|
18
|
+
# an empty Hash if no CONTENT_TYPE or media-type parameters were
|
|
19
|
+
# provided. e.g., when the CONTENT_TYPE is "text/plain;charset=utf-8",
|
|
20
|
+
# this method responds with the following Hash:
|
|
21
|
+
# { 'charset' => 'utf-8' }
|
|
22
|
+
def params(content_type)
|
|
23
|
+
return {} if content_type.nil?
|
|
24
|
+
Hash[*content_type.split(SPLIT_PATTERN)[1..-1].
|
|
25
|
+
collect { |s| s.split('=', 2) }.
|
|
26
|
+
map { |k, v| [k.downcase, strip_doublequotes(v)] }.flatten]
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def strip_doublequotes(str)
|
|
32
|
+
(str[0] == ?" && str[-1] == ?") ? str[1..-2] : str
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
require 'rack/request'
|
|
2
2
|
require 'rack/utils'
|
|
3
3
|
require 'rack/mock'
|
|
4
|
+
require 'mime-types'
|
|
4
5
|
|
|
5
6
|
module JSONAPIonify::Api
|
|
6
7
|
class Server::Request < Rack::Request
|
|
@@ -15,7 +16,7 @@ module JSONAPIonify::Api
|
|
|
15
16
|
end.each_with_object({}) do |(name, value), hash|
|
|
16
17
|
hash[name[5..-1].gsub('_', '-').downcase] = value
|
|
17
18
|
end
|
|
18
|
-
env_headers['content-type'] = content_type
|
|
19
|
+
env_headers['content-type'] = content_type if content_type
|
|
19
20
|
Rack::Utils::HeaderHash.new(env_headers)
|
|
20
21
|
end
|
|
21
22
|
|
|
@@ -44,23 +45,36 @@ module JSONAPIonify::Api
|
|
|
44
45
|
body.rewind
|
|
45
46
|
end
|
|
46
47
|
|
|
48
|
+
def extension
|
|
49
|
+
ext = File.extname(path)
|
|
50
|
+
return nil unless ext
|
|
51
|
+
ext = ext[0] == '.' ? ext[1..-1] : ext
|
|
52
|
+
ext.to_s.empty? ? nil : ext
|
|
53
|
+
end
|
|
54
|
+
|
|
47
55
|
def content_type
|
|
48
56
|
super.try(:split, ';').try(:first)
|
|
49
57
|
end
|
|
50
58
|
|
|
51
|
-
def
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
media_type_params.reject! { |mtp| mtp.start_with? 'q=' }
|
|
60
|
-
[mime, *media_type_params].join(';')
|
|
59
|
+
def accept_params
|
|
60
|
+
@accept_params ||= begin
|
|
61
|
+
ext_mime = MIME::Types.type_for(path)[0]&.content_type
|
|
62
|
+
accepts = (headers['accept'] || ext_mime || '*/*').split(',')
|
|
63
|
+
types = [ext_mime].compact | accepts
|
|
64
|
+
types.each_with_object({}) do |type, list|
|
|
65
|
+
list[Server::MediaType.type(type)] = Server::MediaType.params(type)
|
|
66
|
+
end
|
|
61
67
|
end
|
|
62
68
|
end
|
|
63
69
|
|
|
70
|
+
def jsonapi_params
|
|
71
|
+
accept_params['application/vnd.api+json'] || {}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def accept
|
|
75
|
+
@accept ||= accept_params.keys
|
|
76
|
+
end
|
|
77
|
+
|
|
64
78
|
def authorizations
|
|
65
79
|
parts = headers['authorization'].to_s.split(' ')
|
|
66
80
|
parts.length == 2 ? Rack::Utils::HeaderHash.new([parts].to_h) : {}
|
|
@@ -23,17 +23,21 @@ module JSONAPIonify::Api
|
|
|
23
23
|
@api = api
|
|
24
24
|
@request = Request.new(env)
|
|
25
25
|
request.path_info.split('/').tap(&:shift).tap do |parts|
|
|
26
|
+
parts[-1] = File.basename(parts[-1], File.extname(parts[-1])) if parts[-1]
|
|
26
27
|
@resource, @id, @relationship, @relationship_name, *@more = parts
|
|
27
|
-
request.env['jsonapionify.resource_name'] = @resource if @resource
|
|
28
|
-
request.env['jsonapionify.resource'] = resource if @resource
|
|
29
|
-
request.env['jsonapionify.id'] = @id if @id
|
|
30
28
|
end
|
|
31
29
|
end
|
|
32
30
|
|
|
33
31
|
def response
|
|
32
|
+
request.env['jsonapionify.resource_name'] = @resource if @resource
|
|
33
|
+
request.env['jsonapionify.resource'] = resource if @resource
|
|
34
|
+
request.env['jsonapionify.id'] = @id if @id
|
|
34
35
|
@resource ? resource.process(request) : api_index
|
|
35
36
|
rescue Errors::ResourceNotFound
|
|
36
|
-
|
|
37
|
+
resource = @resource
|
|
38
|
+
api.http_error(:not_found, request) do
|
|
39
|
+
detail "Resource not found: #{resource}"
|
|
40
|
+
end
|
|
37
41
|
end
|
|
38
42
|
|
|
39
43
|
private
|