sanger-jsonapi-resources 0.1.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.
- checksums.yaml +7 -0
- data/LICENSE.txt +22 -0
- data/README.md +53 -0
- data/lib/generators/jsonapi/USAGE +13 -0
- data/lib/generators/jsonapi/controller_generator.rb +14 -0
- data/lib/generators/jsonapi/resource_generator.rb +14 -0
- data/lib/generators/jsonapi/templates/jsonapi_controller.rb +4 -0
- data/lib/generators/jsonapi/templates/jsonapi_resource.rb +4 -0
- data/lib/jsonapi/acts_as_resource_controller.rb +320 -0
- data/lib/jsonapi/cached_resource_fragment.rb +127 -0
- data/lib/jsonapi/callbacks.rb +51 -0
- data/lib/jsonapi/compiled_json.rb +36 -0
- data/lib/jsonapi/configuration.rb +258 -0
- data/lib/jsonapi/error.rb +47 -0
- data/lib/jsonapi/error_codes.rb +60 -0
- data/lib/jsonapi/exceptions.rb +563 -0
- data/lib/jsonapi/formatter.rb +169 -0
- data/lib/jsonapi/include_directives.rb +100 -0
- data/lib/jsonapi/link_builder.rb +152 -0
- data/lib/jsonapi/mime_types.rb +41 -0
- data/lib/jsonapi/naive_cache.rb +30 -0
- data/lib/jsonapi/operation.rb +24 -0
- data/lib/jsonapi/operation_dispatcher.rb +88 -0
- data/lib/jsonapi/operation_result.rb +65 -0
- data/lib/jsonapi/operation_results.rb +35 -0
- data/lib/jsonapi/paginator.rb +209 -0
- data/lib/jsonapi/processor.rb +328 -0
- data/lib/jsonapi/relationship.rb +94 -0
- data/lib/jsonapi/relationship_builder.rb +167 -0
- data/lib/jsonapi/request_parser.rb +678 -0
- data/lib/jsonapi/resource.rb +1255 -0
- data/lib/jsonapi/resource_controller.rb +5 -0
- data/lib/jsonapi/resource_controller_metal.rb +16 -0
- data/lib/jsonapi/resource_serializer.rb +531 -0
- data/lib/jsonapi/resources/version.rb +5 -0
- data/lib/jsonapi/response_document.rb +135 -0
- data/lib/jsonapi/routing_ext.rb +262 -0
- data/lib/jsonapi-resources.rb +27 -0
- metadata +223 -0
@@ -0,0 +1,169 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class Formatter
|
3
|
+
class << self
|
4
|
+
def format(arg)
|
5
|
+
arg.to_s
|
6
|
+
end
|
7
|
+
|
8
|
+
def unformat(arg)
|
9
|
+
arg
|
10
|
+
end
|
11
|
+
|
12
|
+
def cached
|
13
|
+
return FormatterWrapperCache.new(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
def uncached
|
17
|
+
return self
|
18
|
+
end
|
19
|
+
|
20
|
+
def formatter_for(format)
|
21
|
+
"#{format.to_s.camelize}Formatter".safe_constantize
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
class KeyFormatter < Formatter
|
27
|
+
class << self
|
28
|
+
def format(key)
|
29
|
+
super
|
30
|
+
end
|
31
|
+
|
32
|
+
def unformat(formatted_key)
|
33
|
+
super
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
class RouteFormatter < Formatter
|
39
|
+
class << self
|
40
|
+
def format(route)
|
41
|
+
super
|
42
|
+
end
|
43
|
+
|
44
|
+
def unformat(formatted_route)
|
45
|
+
super
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
class ValueFormatter < Formatter
|
51
|
+
class << self
|
52
|
+
def format(raw_value)
|
53
|
+
super(raw_value)
|
54
|
+
end
|
55
|
+
|
56
|
+
def unformat(value)
|
57
|
+
super(value)
|
58
|
+
end
|
59
|
+
|
60
|
+
def value_formatter_for(type)
|
61
|
+
"#{type.to_s.camelize}ValueFormatter".safe_constantize
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Warning: Not thread-safe. Wrap in ThreadLocalVar as needed.
|
67
|
+
class FormatterWrapperCache
|
68
|
+
attr_reader :formatter_klass
|
69
|
+
|
70
|
+
def initialize(formatter_klass)
|
71
|
+
@formatter_klass = formatter_klass
|
72
|
+
@format_cache = NaiveCache.new{|arg| formatter_klass.format(arg) }
|
73
|
+
@unformat_cache = NaiveCache.new{|arg| formatter_klass.unformat(arg) }
|
74
|
+
end
|
75
|
+
|
76
|
+
def format(arg)
|
77
|
+
@format_cache.get(arg)
|
78
|
+
end
|
79
|
+
|
80
|
+
def unformat(arg)
|
81
|
+
@unformat_cache.get(arg)
|
82
|
+
end
|
83
|
+
|
84
|
+
def cached
|
85
|
+
self
|
86
|
+
end
|
87
|
+
|
88
|
+
def uncached
|
89
|
+
return @formatter_klass
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
class UnderscoredKeyFormatter < JSONAPI::KeyFormatter
|
95
|
+
end
|
96
|
+
|
97
|
+
class CamelizedKeyFormatter < JSONAPI::KeyFormatter
|
98
|
+
class << self
|
99
|
+
def format(key)
|
100
|
+
super.camelize(:lower)
|
101
|
+
end
|
102
|
+
|
103
|
+
def unformat(formatted_key)
|
104
|
+
formatted_key.to_s.underscore
|
105
|
+
end
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
class DasherizedKeyFormatter < JSONAPI::KeyFormatter
|
110
|
+
class << self
|
111
|
+
def format(key)
|
112
|
+
super.underscore.dasherize
|
113
|
+
end
|
114
|
+
|
115
|
+
def unformat(formatted_key)
|
116
|
+
formatted_key.to_s.underscore
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
class DefaultValueFormatter < JSONAPI::ValueFormatter
|
122
|
+
class << self
|
123
|
+
def format(raw_value)
|
124
|
+
case raw_value
|
125
|
+
when Date, Time, DateTime, ActiveSupport::TimeWithZone, BigDecimal
|
126
|
+
# Use the as_json methods added to various base classes by ActiveSupport
|
127
|
+
return raw_value.as_json
|
128
|
+
else
|
129
|
+
return raw_value
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
class IdValueFormatter < JSONAPI::ValueFormatter
|
136
|
+
class << self
|
137
|
+
def format(raw_value)
|
138
|
+
return if raw_value.nil?
|
139
|
+
raw_value.to_s
|
140
|
+
end
|
141
|
+
end
|
142
|
+
end
|
143
|
+
|
144
|
+
class UnderscoredRouteFormatter < JSONAPI::RouteFormatter
|
145
|
+
end
|
146
|
+
|
147
|
+
class CamelizedRouteFormatter < JSONAPI::RouteFormatter
|
148
|
+
class << self
|
149
|
+
def format(route)
|
150
|
+
super.camelize(:lower)
|
151
|
+
end
|
152
|
+
|
153
|
+
def unformat(formatted_route)
|
154
|
+
formatted_route.to_s.underscore
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
class DasherizedRouteFormatter < JSONAPI::RouteFormatter
|
160
|
+
class << self
|
161
|
+
def format(route)
|
162
|
+
super.dasherize
|
163
|
+
end
|
164
|
+
|
165
|
+
def unformat(formatted_route)
|
166
|
+
formatted_route.to_s.underscore
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,100 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class IncludeDirectives
|
3
|
+
# Construct an IncludeDirectives Hash from an array of dot separated include strings.
|
4
|
+
# For example ['posts.comments.tags']
|
5
|
+
# will transform into =>
|
6
|
+
# {
|
7
|
+
# posts:{
|
8
|
+
# include:true,
|
9
|
+
# include_related:{
|
10
|
+
# comments:{
|
11
|
+
# include:true,
|
12
|
+
# include_related:{
|
13
|
+
# tags:{
|
14
|
+
# include:true
|
15
|
+
# }
|
16
|
+
# }
|
17
|
+
# }
|
18
|
+
# }
|
19
|
+
# }
|
20
|
+
# }
|
21
|
+
|
22
|
+
def initialize(resource_klass, includes_array, force_eager_load: false)
|
23
|
+
@resource_klass = resource_klass
|
24
|
+
@force_eager_load = force_eager_load
|
25
|
+
@include_directives_hash = { include_related: {} }
|
26
|
+
includes_array.each do |include|
|
27
|
+
parse_include(include)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def include_directives
|
32
|
+
@include_directives_hash
|
33
|
+
end
|
34
|
+
|
35
|
+
def model_includes
|
36
|
+
get_includes(@include_directives_hash)
|
37
|
+
end
|
38
|
+
|
39
|
+
def paths
|
40
|
+
delve_paths(get_includes(@include_directives_hash, false))
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def get_related(current_path)
|
46
|
+
current = @include_directives_hash
|
47
|
+
current_resource_klass = @resource_klass
|
48
|
+
current_path.split('.').each do |fragment|
|
49
|
+
fragment = fragment.to_sym
|
50
|
+
|
51
|
+
if current_resource_klass
|
52
|
+
current_relationship = current_resource_klass._relationships[fragment]
|
53
|
+
current_resource_klass = current_relationship.try(:resource_klass)
|
54
|
+
else
|
55
|
+
warn "[RELATIONSHIP NOT FOUND] Relationship could not be found for #{current_path}."
|
56
|
+
end
|
57
|
+
|
58
|
+
include_in_join = @force_eager_load || !current_relationship || current_relationship.eager_load_on_include
|
59
|
+
|
60
|
+
current[:include_related][fragment] ||= { include: false, include_related: {}, include_in_join: include_in_join }
|
61
|
+
current = current[:include_related][fragment]
|
62
|
+
end
|
63
|
+
current
|
64
|
+
end
|
65
|
+
|
66
|
+
def get_includes(directive, only_joined_includes = true)
|
67
|
+
ir = directive[:include_related]
|
68
|
+
ir = ir.select { |k,v| v[:include_in_join] } if only_joined_includes
|
69
|
+
|
70
|
+
ir.map do |name, sub_directive|
|
71
|
+
sub = get_includes(sub_directive, only_joined_includes)
|
72
|
+
sub.any? ? { name => sub } : name
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
def parse_include(include)
|
77
|
+
parts = include.split('.')
|
78
|
+
local_path = ''
|
79
|
+
|
80
|
+
parts.each do |name|
|
81
|
+
local_path += local_path.length > 0 ? ".#{name}" : name
|
82
|
+
related = get_related(local_path)
|
83
|
+
related[:include] = true
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
def delve_paths(obj)
|
88
|
+
case obj
|
89
|
+
when Array
|
90
|
+
obj.map{|elem| delve_paths(elem)}.flatten(1)
|
91
|
+
when Hash
|
92
|
+
obj.map{|k,v| [[k]] + delve_paths(v).map{|path| [k] + path } }.flatten(1)
|
93
|
+
when Symbol, String
|
94
|
+
[[obj]]
|
95
|
+
else
|
96
|
+
raise "delve_paths cannot descend into #{obj.class.name}"
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
100
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class LinkBuilder
|
3
|
+
attr_reader :base_url,
|
4
|
+
:primary_resource_klass,
|
5
|
+
:route_formatter,
|
6
|
+
:engine_name
|
7
|
+
|
8
|
+
def initialize(config = {})
|
9
|
+
@base_url = config[:base_url]
|
10
|
+
@primary_resource_klass = config[:primary_resource_klass]
|
11
|
+
@route_formatter = config[:route_formatter]
|
12
|
+
@engine_name = build_engine_name
|
13
|
+
|
14
|
+
# Warning: These make LinkBuilder non-thread-safe. That's not a problem with the
|
15
|
+
# request-specific way it's currently used, though.
|
16
|
+
@resources_path_cache = JSONAPI::NaiveCache.new do |source_klass|
|
17
|
+
formatted_module_path_from_class(source_klass) + format_route(source_klass._type.to_s)
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def engine?
|
22
|
+
!!@engine_name
|
23
|
+
end
|
24
|
+
|
25
|
+
def primary_resources_url
|
26
|
+
if engine?
|
27
|
+
engine_primary_resources_url
|
28
|
+
else
|
29
|
+
regular_primary_resources_url
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def query_link(query_params)
|
34
|
+
"#{ primary_resources_url }?#{ query_params.to_query }"
|
35
|
+
end
|
36
|
+
|
37
|
+
def relationships_related_link(source, relationship, query_params = {})
|
38
|
+
url = "#{ self_link(source) }/#{ route_for_relationship(relationship) }"
|
39
|
+
url = "#{ url }?#{ query_params.to_query }" if query_params.present?
|
40
|
+
url
|
41
|
+
end
|
42
|
+
|
43
|
+
def relationships_self_link(source, relationship)
|
44
|
+
"#{ self_link(source) }/relationships/#{ route_for_relationship(relationship) }"
|
45
|
+
end
|
46
|
+
|
47
|
+
def self_link(source)
|
48
|
+
if engine?
|
49
|
+
engine_resource_url(source)
|
50
|
+
else
|
51
|
+
regular_resource_url(source)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def build_engine_name
|
58
|
+
scopes = module_scopes_from_class(primary_resource_klass)
|
59
|
+
|
60
|
+
begin
|
61
|
+
unless scopes.empty?
|
62
|
+
"#{ scopes.first.to_s.camelize }::Engine".safe_constantize
|
63
|
+
end
|
64
|
+
rescue LoadError => e
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def engine_path_from_resource_class(klass)
|
70
|
+
path_name = engine_resources_path_name_from_class(klass)
|
71
|
+
engine_name.routes.url_helpers.public_send(path_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def engine_primary_resources_path
|
75
|
+
engine_path_from_resource_class(primary_resource_klass)
|
76
|
+
end
|
77
|
+
|
78
|
+
def engine_primary_resources_url
|
79
|
+
"#{ base_url }#{ engine_primary_resources_path }"
|
80
|
+
end
|
81
|
+
|
82
|
+
def engine_resource_path(source)
|
83
|
+
resource_path_name = engine_resource_path_name_from_source(source)
|
84
|
+
engine_name.routes.url_helpers.public_send(resource_path_name, source.id)
|
85
|
+
end
|
86
|
+
|
87
|
+
def engine_resource_path_name_from_source(source)
|
88
|
+
scopes = module_scopes_from_class(source.class)[1..-1]
|
89
|
+
base_path_name = scopes.map { |scope| scope.underscore }.join("_")
|
90
|
+
end_path_name = source.class._type.to_s.singularize
|
91
|
+
[base_path_name, end_path_name, "path"].reject(&:blank?).join("_")
|
92
|
+
end
|
93
|
+
|
94
|
+
def engine_resource_url(source)
|
95
|
+
"#{ base_url }#{ engine_resource_path(source) }"
|
96
|
+
end
|
97
|
+
|
98
|
+
def engine_resources_path_name_from_class(klass)
|
99
|
+
scopes = module_scopes_from_class(klass)[1..-1]
|
100
|
+
base_path_name = scopes.map { |scope| scope.underscore }.join("_")
|
101
|
+
end_path_name = klass._type.to_s
|
102
|
+
|
103
|
+
if base_path_name.blank?
|
104
|
+
"#{ end_path_name }_path"
|
105
|
+
else
|
106
|
+
"#{ base_path_name }_#{ end_path_name }_path"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def format_route(route)
|
111
|
+
route_formatter.format(route)
|
112
|
+
end
|
113
|
+
|
114
|
+
def formatted_module_path_from_class(klass)
|
115
|
+
scopes = module_scopes_from_class(klass)
|
116
|
+
|
117
|
+
unless scopes.empty?
|
118
|
+
"/#{ scopes.map{ |scope| format_route(scope.to_s.underscore) }.join('/') }/"
|
119
|
+
else
|
120
|
+
"/"
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def module_scopes_from_class(klass)
|
125
|
+
klass.name.to_s.split("::")[0...-1]
|
126
|
+
end
|
127
|
+
|
128
|
+
def regular_resources_path(source_klass)
|
129
|
+
@resources_path_cache.get(source_klass)
|
130
|
+
end
|
131
|
+
|
132
|
+
def regular_primary_resources_path
|
133
|
+
regular_resources_path(primary_resource_klass)
|
134
|
+
end
|
135
|
+
|
136
|
+
def regular_primary_resources_url
|
137
|
+
"#{ base_url }#{ regular_primary_resources_path }"
|
138
|
+
end
|
139
|
+
|
140
|
+
def regular_resource_path(source)
|
141
|
+
"#{regular_resources_path(source.class)}/#{source.id}"
|
142
|
+
end
|
143
|
+
|
144
|
+
def regular_resource_url(source)
|
145
|
+
"#{ base_url }#{ regular_resource_path(source) }"
|
146
|
+
end
|
147
|
+
|
148
|
+
def route_for_relationship(relationship)
|
149
|
+
format_route(relationship.name)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
require 'json'
|
2
|
+
|
3
|
+
module JSONAPI
|
4
|
+
MEDIA_TYPE = 'application/vnd.api+json'
|
5
|
+
|
6
|
+
module MimeTypes
|
7
|
+
def self.install
|
8
|
+
Mime::Type.register JSONAPI::MEDIA_TYPE, :api_json
|
9
|
+
|
10
|
+
# :nocov:
|
11
|
+
if Rails::VERSION::MAJOR >= 5
|
12
|
+
parsers = ActionDispatch::Request.parameter_parsers.merge(
|
13
|
+
Mime::Type.lookup(JSONAPI::MEDIA_TYPE).symbol => parser
|
14
|
+
)
|
15
|
+
ActionDispatch::Request.parameter_parsers = parsers
|
16
|
+
else
|
17
|
+
ActionDispatch::ParamsParser::DEFAULT_PARSERS[Mime::Type.lookup(JSONAPI::MEDIA_TYPE)] = parser
|
18
|
+
end
|
19
|
+
# :nocov:
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.parser
|
23
|
+
lambda do |body|
|
24
|
+
begin
|
25
|
+
data = JSON.parse(body)
|
26
|
+
if data.is_a?(Hash)
|
27
|
+
data.with_indifferent_access
|
28
|
+
else
|
29
|
+
fail JSONAPI::Exceptions::InvalidRequestFormat.new
|
30
|
+
end
|
31
|
+
rescue JSON::ParserError => e
|
32
|
+
{ _parser_exception: JSONAPI::Exceptions::BadRequest.new(e.to_s) }
|
33
|
+
rescue => e
|
34
|
+
{ _parser_exception: e }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
MimeTypes.install
|
41
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
|
3
|
+
# Cache which memoizes the given block.
|
4
|
+
#
|
5
|
+
# It's "naive" because it clears the least-recently-inserted cache entry
|
6
|
+
# rather than the least-recently-used. This makes lookups faster but cache
|
7
|
+
# misses more frequent after cleanups. Therefore you the best time to use
|
8
|
+
# this cache is when you expect only a small number of unique lookup keys, so
|
9
|
+
# that the cache never has to clear.
|
10
|
+
#
|
11
|
+
# Also, it's not thread safe (although jsonapi-resources is careful to only
|
12
|
+
# use it in a thread safe way).
|
13
|
+
class NaiveCache
|
14
|
+
def initialize(cap = 10000, &calculator)
|
15
|
+
@cap = cap
|
16
|
+
@data = {}
|
17
|
+
@calculator = calculator
|
18
|
+
end
|
19
|
+
|
20
|
+
def get(key)
|
21
|
+
found = true
|
22
|
+
value = @data.fetch(key) { found = false }
|
23
|
+
return value if found
|
24
|
+
value = @calculator.call(key)
|
25
|
+
@data[key] = value
|
26
|
+
@data.shift if @data.length > @cap
|
27
|
+
return value
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class Operation
|
3
|
+
attr_reader :resource_klass, :operation_type, :options
|
4
|
+
|
5
|
+
def initialize(operation_type, resource_klass, options)
|
6
|
+
@operation_type = operation_type
|
7
|
+
@resource_klass = resource_klass
|
8
|
+
@options = options
|
9
|
+
end
|
10
|
+
|
11
|
+
def transactional?
|
12
|
+
JSONAPI::Processor._processor_from_resource_type(resource_klass).transactional_operation_type?(operation_type)
|
13
|
+
end
|
14
|
+
|
15
|
+
def process
|
16
|
+
processor.process
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
def processor
|
21
|
+
JSONAPI::Processor.processor_instance_for(resource_klass, operation_type, options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class OperationDispatcher
|
3
|
+
|
4
|
+
def initialize(transaction: lambda { |&block| block.yield },
|
5
|
+
rollback: lambda { },
|
6
|
+
server_error_callbacks: [])
|
7
|
+
|
8
|
+
@transaction = transaction
|
9
|
+
@rollback = rollback
|
10
|
+
@server_error_callbacks = server_error_callbacks
|
11
|
+
end
|
12
|
+
|
13
|
+
def process(operations)
|
14
|
+
results = JSONAPI::OperationResults.new
|
15
|
+
|
16
|
+
# Use transactions if more than one operation and if one of the operations can be transactional
|
17
|
+
# Even if transactional transactions won't be used unless the derived OperationsProcessor supports them.
|
18
|
+
transactional = false
|
19
|
+
|
20
|
+
operations.each do |operation|
|
21
|
+
transactional |= operation.transactional?
|
22
|
+
end if JSONAPI.configuration.allow_transactions
|
23
|
+
|
24
|
+
transaction(transactional) do
|
25
|
+
# Links and meta data global to the set of operations
|
26
|
+
operations_meta = {}
|
27
|
+
operations_links = {}
|
28
|
+
operations.each do |operation|
|
29
|
+
results.add_result(process_operation(operation))
|
30
|
+
rollback(transactional) if results.has_errors?
|
31
|
+
end
|
32
|
+
results.meta = operations_meta
|
33
|
+
results.links = operations_links
|
34
|
+
end
|
35
|
+
results
|
36
|
+
end
|
37
|
+
|
38
|
+
private
|
39
|
+
|
40
|
+
def transaction(transactional)
|
41
|
+
if transactional
|
42
|
+
@transaction.call do
|
43
|
+
yield
|
44
|
+
end
|
45
|
+
else
|
46
|
+
yield
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def rollback(transactional)
|
51
|
+
if transactional
|
52
|
+
@rollback.call
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def process_operation(operation)
|
57
|
+
with_default_handling do
|
58
|
+
operation.process
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def with_default_handling(&block)
|
63
|
+
block.yield
|
64
|
+
rescue => e
|
65
|
+
if JSONAPI.configuration.exception_class_whitelisted?(e)
|
66
|
+
raise e
|
67
|
+
else
|
68
|
+
@server_error_callbacks.each { |callback|
|
69
|
+
safe_run_callback(callback, e)
|
70
|
+
}
|
71
|
+
|
72
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
73
|
+
Rails.logger.error { "Internal Server Error: #{e.message} #{e.backtrace.join("\n")}" }
|
74
|
+
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def safe_run_callback(callback, error)
|
79
|
+
begin
|
80
|
+
callback.call(error)
|
81
|
+
rescue => e
|
82
|
+
Rails.logger.error { "Error in error handling callback: #{e.message} #{e.backtrace.join("\n")}" }
|
83
|
+
internal_server_error = JSONAPI::Exceptions::InternalServerError.new(e)
|
84
|
+
return JSONAPI::ErrorsOperationResult.new(internal_server_error.errors[0].code, internal_server_error.errors)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class OperationResult
|
3
|
+
attr_accessor :code
|
4
|
+
attr_accessor :meta
|
5
|
+
attr_accessor :links
|
6
|
+
attr_accessor :options
|
7
|
+
|
8
|
+
def initialize(code, options = {})
|
9
|
+
@code = code
|
10
|
+
@options = options
|
11
|
+
@meta = options.fetch(:meta, {})
|
12
|
+
@links = options.fetch(:links, {})
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
class ErrorsOperationResult < OperationResult
|
17
|
+
attr_accessor :errors
|
18
|
+
|
19
|
+
def initialize(code, errors, options = {})
|
20
|
+
@errors = errors
|
21
|
+
super(code, options)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
class ResourceOperationResult < OperationResult
|
26
|
+
attr_accessor :resource
|
27
|
+
|
28
|
+
def initialize(code, resource, options = {})
|
29
|
+
@resource = resource
|
30
|
+
super(code, options)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
class ResourcesOperationResult < OperationResult
|
35
|
+
attr_accessor :resources, :pagination_params, :record_count, :page_count
|
36
|
+
|
37
|
+
def initialize(code, resources, options = {})
|
38
|
+
@resources = resources
|
39
|
+
@pagination_params = options.fetch(:pagination_params, {})
|
40
|
+
@record_count = options[:record_count]
|
41
|
+
@page_count = options[:page_count]
|
42
|
+
super(code, options)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
class RelatedResourcesOperationResult < ResourcesOperationResult
|
47
|
+
attr_accessor :source_resource, :_type
|
48
|
+
|
49
|
+
def initialize(code, source_resource, type, resources, options = {})
|
50
|
+
@source_resource = source_resource
|
51
|
+
@_type = type
|
52
|
+
super(code, resources, options)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
class LinksObjectOperationResult < OperationResult
|
57
|
+
attr_accessor :parent_resource, :relationship
|
58
|
+
|
59
|
+
def initialize(code, parent_resource, relationship, options = {})
|
60
|
+
@parent_resource = parent_resource
|
61
|
+
@relationship = relationship
|
62
|
+
super(code, options)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
class OperationResults
|
3
|
+
attr_accessor :results
|
4
|
+
attr_accessor :meta
|
5
|
+
attr_accessor :links
|
6
|
+
|
7
|
+
def initialize
|
8
|
+
@results = []
|
9
|
+
@has_errors = false
|
10
|
+
@meta = {}
|
11
|
+
@links = {}
|
12
|
+
end
|
13
|
+
|
14
|
+
def add_result(result)
|
15
|
+
@has_errors = true if result.is_a?(JSONAPI::ErrorsOperationResult)
|
16
|
+
@results.push(result)
|
17
|
+
end
|
18
|
+
|
19
|
+
def has_errors?
|
20
|
+
@has_errors
|
21
|
+
end
|
22
|
+
|
23
|
+
def all_errors
|
24
|
+
errors = []
|
25
|
+
if @has_errors
|
26
|
+
@results.each do |result|
|
27
|
+
if result.is_a?(JSONAPI::ErrorsOperationResult)
|
28
|
+
errors.concat(result.errors)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
errors
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|