infinum_json_api_setup 0.0.3
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/.gitignore +6 -0
- data/.overcommit.yml +26 -0
- data/.rspec +2 -0
- data/.rubocop.yml +29 -0
- data/.ruby-version +1 -0
- data/.simplecov +3 -0
- data/Gemfile +10 -0
- data/Gemfile.lock +268 -0
- data/README.md +156 -0
- data/Rakefile +1 -0
- data/bin/setup +18 -0
- data/infinum_json_api_setup.gemspec +30 -0
- data/lib/generators/infinum_json_api_setup/install_generator.rb +12 -0
- data/lib/generators/infinum_json_api_setup/templates/config/locales/json_api.en.yml +21 -0
- data/lib/infinum_json_api_setup/error.rb +61 -0
- data/lib/infinum_json_api_setup/json_api/content_negotiation.rb +29 -0
- data/lib/infinum_json_api_setup/json_api/error_handling.rb +49 -0
- data/lib/infinum_json_api_setup/json_api/error_serializer.rb +72 -0
- data/lib/infinum_json_api_setup/json_api/request_parsing.rb +17 -0
- data/lib/infinum_json_api_setup/json_api/responder.rb +31 -0
- data/lib/infinum_json_api_setup/json_api/serializer_options.rb +76 -0
- data/lib/infinum_json_api_setup/rails.rb +28 -0
- data/lib/infinum_json_api_setup/rspec/helpers/request_helper.rb +80 -0
- data/lib/infinum_json_api_setup/rspec/helpers/response_helper.rb +56 -0
- data/lib/infinum_json_api_setup/rspec/matchers/have_empty_data.rb +32 -0
- data/lib/infinum_json_api_setup/rspec/matchers/have_error_pointer.rb +37 -0
- data/lib/infinum_json_api_setup/rspec/matchers/have_resource_count_of.rb +37 -0
- data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_ids.rb +51 -0
- data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_sorted.rb +21 -0
- data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_string_ids.rb +17 -0
- data/lib/infinum_json_api_setup/rspec/matchers/include_error_detail.rb +37 -0
- data/lib/infinum_json_api_setup/rspec/matchers/include_related_resource.rb +42 -0
- data/lib/infinum_json_api_setup/rspec/matchers/json_body_matcher.rb +42 -0
- data/lib/infinum_json_api_setup/rspec/matchers/schema_matchers.rb +29 -0
- data/lib/infinum_json_api_setup/rspec.rb +21 -0
- data/lib/infinum_json_api_setup/version.rb +3 -0
- data/lib/infinum_json_api_setup.rb +16 -0
- data/spec/controllers/api/v1/base_controller_spec.rb +9 -0
- data/spec/dummy/Rakefile +6 -0
- data/spec/dummy/app/assets/config/manifest.js +1 -0
- data/spec/dummy/app/assets/stylesheets/application.css +15 -0
- data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
- data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
- data/spec/dummy/app/controllers/api/v1/base_controller.rb +11 -0
- data/spec/dummy/app/controllers/api/v1/locations_controller.rb +64 -0
- data/spec/dummy/app/controllers/application_controller.rb +2 -0
- data/spec/dummy/app/controllers/concerns/.keep +0 -0
- data/spec/dummy/app/javascript/packs/application.js +15 -0
- data/spec/dummy/app/jobs/application_job.rb +7 -0
- data/spec/dummy/app/mailers/application_mailer.rb +4 -0
- data/spec/dummy/app/models/application_record.rb +3 -0
- data/spec/dummy/app/models/concerns/.keep +0 -0
- data/spec/dummy/app/models/location.rb +38 -0
- data/spec/dummy/app/models/location_label.rb +3 -0
- data/spec/dummy/app/policies/application_policy.rb +51 -0
- data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
- data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
- data/spec/dummy/app/web/api/v1/locations/label_serializer.rb +13 -0
- data/spec/dummy/app/web/api/v1/locations/policy.rb +28 -0
- data/spec/dummy/app/web/api/v1/locations/query.rb +12 -0
- data/spec/dummy/app/web/api/v1/locations/serializer.rb +15 -0
- data/spec/dummy/bin/rails +4 -0
- data/spec/dummy/bin/rake +4 -0
- data/spec/dummy/bin/setup +33 -0
- data/spec/dummy/config/application.rb +39 -0
- data/spec/dummy/config/boot.rb +5 -0
- data/spec/dummy/config/cable.yml +10 -0
- data/spec/dummy/config/database.yml +15 -0
- data/spec/dummy/config/environment.rb +5 -0
- data/spec/dummy/config/environments/development.rb +65 -0
- data/spec/dummy/config/environments/production.rb +114 -0
- data/spec/dummy/config/environments/test.rb +60 -0
- data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
- data/spec/dummy/config/initializers/backtrace_silencers.rb +10 -0
- data/spec/dummy/config/initializers/cors.rb +16 -0
- data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
- data/spec/dummy/config/initializers/inflections.rb +16 -0
- data/spec/dummy/config/initializers/mime_types.rb +4 -0
- data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
- data/spec/dummy/config/locales/en.yml +33 -0
- data/spec/dummy/config/locales/json_api.en.yml +21 -0
- data/spec/dummy/config/puma.rb +43 -0
- data/spec/dummy/config/routes.rb +7 -0
- data/spec/dummy/config/storage.yml +34 -0
- data/spec/dummy/config.ru +6 -0
- data/spec/dummy/db/migrate/20210709100750_create_locations.rb +10 -0
- data/spec/dummy/db/migrate/20210714104736_create_location_labels.rb +10 -0
- data/spec/dummy/db/schema.rb +34 -0
- data/spec/dummy/log/.keep +0 -0
- data/spec/dummy/storage/.keep +0 -0
- data/spec/factories/location.rb +11 -0
- data/spec/factories/location_label.rb +6 -0
- data/spec/infinum_json_api_setup/rspec/matchers/have_empty_data_spec.rb +43 -0
- data/spec/infinum_json_api_setup/rspec/matchers/have_error_pointer_spec.rb +43 -0
- data/spec/infinum_json_api_setup/rspec/matchers/have_resource_count_of_spec.rb +43 -0
- data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_sorted_spec.rb +53 -0
- data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_spec.rb +49 -0
- data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_string_ids_spec.rb +49 -0
- data/spec/infinum_json_api_setup/rspec/matchers/include_error_detail_spec.rb +43 -0
- data/spec/infinum_json_api_setup/rspec/matchers/include_related_resource_spec.rb +43 -0
- data/spec/infinum_json_api_setup/rspec/matchers/util/body_parser.rb +30 -0
- data/spec/rails_helper.rb +77 -0
- data/spec/requests/api/v1/content_negotiation_spec.rb +29 -0
- data/spec/requests/api/v1/error_handling_spec.rb +114 -0
- data/spec/requests/api/v1/responder_spec.rb +91 -0
- data/spec/requests/api/v1/serializer_options_spec.rb +46 -0
- data/spec/spec_helper.rb +94 -0
- data/spec/support/factory_bot.rb +7 -0
- data/spec/support/infinum_json_api_setup.rb +1 -0
- data/spec/support/rspec_expectations.rb +5 -0
- data/spec/support/test_helpers/matchers/response.rb +17 -0
- data/spec/support/test_helpers/request.rb +11 -0
- data/spec/support/test_helpers/response.rb +8 -0
- metadata +351 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module JsonApi
|
|
3
|
+
module ContentNegotiation
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do
|
|
7
|
+
before_action :validate_jsonapi_request
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def validate_jsonapi_request
|
|
11
|
+
if !acceptable?
|
|
12
|
+
head :not_acceptable
|
|
13
|
+
elsif !valid_content_type?
|
|
14
|
+
head :unsupported_media_type
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def valid_content_type?
|
|
19
|
+
return true if request.body.size.zero?
|
|
20
|
+
|
|
21
|
+
request.content_type == Mime.fetch(:json_api)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def acceptable?
|
|
25
|
+
request.accept&.split(',')&.include?(Mime.fetch(:json_api))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module JsonApi
|
|
3
|
+
module ErrorHandling
|
|
4
|
+
extend ActiveSupport::Concern
|
|
5
|
+
|
|
6
|
+
included do # rubocop:disable Metrics/BlockLength
|
|
7
|
+
rescue_from ActionController::ParameterMissing do |e|
|
|
8
|
+
message = e.to_s.split("\n").first
|
|
9
|
+
render_error(InfinumJsonApiSetup::Error::BadRequest.new(message: message))
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
rescue_from ActiveRecord::RecordNotFound do
|
|
13
|
+
render_error(InfinumJsonApiSetup::Error::RecordNotFound.new)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
if defined?(Pundit)
|
|
17
|
+
rescue_from Pundit::NotAuthorizedError do
|
|
18
|
+
render_error(InfinumJsonApiSetup::Error::Forbidden.new)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
if defined?(Jsonapi::QueryBuilder)
|
|
23
|
+
rescue_from Jsonapi::QueryBuilder::Mixins::Sort::UnpermittedSortParameters do |e|
|
|
24
|
+
Bugsnag.notify(e)
|
|
25
|
+
render_error(InfinumJsonApiSetup::Error::BadRequest.new(message: e.to_s))
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# we don't want to expose internal details to API consumer
|
|
30
|
+
rescue_from PG::Error do |e|
|
|
31
|
+
Bugsnag.notify(e)
|
|
32
|
+
render_error(InfinumJsonApiSetup::Error::InternalServerError.new)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
rescue_from I18n::InvalidLocale do |e|
|
|
36
|
+
render_error(InfinumJsonApiSetup::Error::BadRequest.new(message: e.to_s))
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
rescue_from ActionDispatch::Http::Parameters::ParseError do |e|
|
|
40
|
+
render_error(InfinumJsonApiSetup::Error::BadRequest.new(message: e.to_s))
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def render_error(error)
|
|
45
|
+
render json_api: error, status: error.http_status
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module JsonApi
|
|
3
|
+
class ErrorSerializer
|
|
4
|
+
# @return [Object]
|
|
5
|
+
attr_reader :error
|
|
6
|
+
|
|
7
|
+
# @return [String]
|
|
8
|
+
delegate :details, to: :error
|
|
9
|
+
|
|
10
|
+
# @param [Object]
|
|
11
|
+
def initialize(error)
|
|
12
|
+
@error = error
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
# @param [String]
|
|
16
|
+
def serialized_json
|
|
17
|
+
ActiveSupport::JSON.encode(serializable_hash)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# @param [Hash]
|
|
21
|
+
def serializable_hash
|
|
22
|
+
{}.tap do |hash|
|
|
23
|
+
hash[:errors] =
|
|
24
|
+
error.details.is_a?(Array) ? serialize_error_array : serialize_error_message
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
private
|
|
29
|
+
|
|
30
|
+
def serialize_error_array
|
|
31
|
+
details.map do |attribute, message|
|
|
32
|
+
{
|
|
33
|
+
status: error.http_status.to_s.humanize(capitalize: false),
|
|
34
|
+
code: error.code,
|
|
35
|
+
title: I18n.t("json_api.errors.#{error.http_status}.title"),
|
|
36
|
+
detail: message,
|
|
37
|
+
source: serialize_source(attribute)
|
|
38
|
+
}
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def serialize_source(attribute)
|
|
43
|
+
# examples:
|
|
44
|
+
# 'attribute_name'.match(/(?:(.+)\.)?([^.]+\z)/).to_a
|
|
45
|
+
# # => ["attribute_name", nil, "attribute_name"]
|
|
46
|
+
# 'relationship.attribute_name'.match(/(?:(.+)\.)?([^.]+\z)/).to_a
|
|
47
|
+
# # => ["relationship.attribute_name", "relationship", "attribute_name"]
|
|
48
|
+
parameter, relationship, attribute = attribute.match(/(?:(.+)\.)?([^.]+\z)/).to_a
|
|
49
|
+
|
|
50
|
+
{}.tap do |hash|
|
|
51
|
+
hash[:parameter] = parameter
|
|
52
|
+
hash[:pointer] = if relationship
|
|
53
|
+
"data/attributes/#{relationship}_attributes/#{attribute}"
|
|
54
|
+
else
|
|
55
|
+
"data/attributes/#{attribute}"
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def serialize_error_message
|
|
61
|
+
[
|
|
62
|
+
{
|
|
63
|
+
status: error.http_status.to_s.humanize(capitalize: false),
|
|
64
|
+
code: error.code,
|
|
65
|
+
title: I18n.t("json_api.errors.#{error.http_status}.title"),
|
|
66
|
+
detail: details
|
|
67
|
+
}
|
|
68
|
+
]
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module JsonApi
|
|
3
|
+
module RequestParsing
|
|
4
|
+
def relationship_children_ids(type)
|
|
5
|
+
data_params.filter_map do |p|
|
|
6
|
+
next if p[:type] != type
|
|
7
|
+
|
|
8
|
+
p[:id].to_i
|
|
9
|
+
end.uniq
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def data_params
|
|
13
|
+
params.to_unsafe_hash[:data]
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module JsonApi
|
|
3
|
+
class Responder < ActionController::Responder
|
|
4
|
+
def to_json_api
|
|
5
|
+
if !get? && has_errors?
|
|
6
|
+
display_errors
|
|
7
|
+
else
|
|
8
|
+
display_resource
|
|
9
|
+
end
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def display_resource
|
|
15
|
+
if get? || patch? || put?
|
|
16
|
+
display resource
|
|
17
|
+
elsif post?
|
|
18
|
+
display resource, status: :created
|
|
19
|
+
elsif delete?
|
|
20
|
+
head :no_content
|
|
21
|
+
else
|
|
22
|
+
raise 'respond_with should be used only for standard REST CRUD actions'
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def json_api_resource_errors
|
|
27
|
+
InfinumJsonApiSetup::Error::UnprocessableEntity.new(object: resource)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module JsonApi
|
|
3
|
+
class SerializerOptions
|
|
4
|
+
# @param [Hash] opts
|
|
5
|
+
# @option opts [Hash] :params
|
|
6
|
+
# @option opts [Object] :pagination_details
|
|
7
|
+
def initialize(params:, pagination_details:)
|
|
8
|
+
@params = params
|
|
9
|
+
@pagination_details = pagination_details
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @return [Hash]
|
|
13
|
+
def build
|
|
14
|
+
{
|
|
15
|
+
meta: meta,
|
|
16
|
+
links: links,
|
|
17
|
+
fields: fields,
|
|
18
|
+
include: include
|
|
19
|
+
}.compact
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
attr_reader :params
|
|
25
|
+
attr_reader :pagination_details
|
|
26
|
+
|
|
27
|
+
def meta
|
|
28
|
+
return {} unless pagination_details
|
|
29
|
+
|
|
30
|
+
{
|
|
31
|
+
current_page: pagination_details.page,
|
|
32
|
+
total_pages: pagination_details.pages,
|
|
33
|
+
total_count: pagination_details.count,
|
|
34
|
+
padding: pagination_details.vars.fetch(:outset).to_i,
|
|
35
|
+
page_size: pagination_details.vars.fetch(:items).to_i,
|
|
36
|
+
max_page_size: Pagy::VARS[:max_items]
|
|
37
|
+
}
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def links
|
|
41
|
+
return {} unless pagination_details
|
|
42
|
+
|
|
43
|
+
{
|
|
44
|
+
self: build_link(pagination_details.page),
|
|
45
|
+
first: build_link(1),
|
|
46
|
+
last: build_link(pagination_details.last),
|
|
47
|
+
prev: build_link(pagination_details.prev),
|
|
48
|
+
next: build_link(pagination_details.next)
|
|
49
|
+
}.compact
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def build_link(page)
|
|
53
|
+
return unless page
|
|
54
|
+
|
|
55
|
+
link_params = params.deep_dup
|
|
56
|
+
link_params[:page] = {
|
|
57
|
+
number: page,
|
|
58
|
+
size: pagination_details.vars.fetch(:items),
|
|
59
|
+
padding: pagination_details.vars.fetch(:outset)
|
|
60
|
+
}.compact
|
|
61
|
+
|
|
62
|
+
Rails.application.routes.url_helpers.url_for(link_params)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def fields
|
|
66
|
+
return nil unless params[:fields]
|
|
67
|
+
|
|
68
|
+
params[:fields].transform_values { |fields| fields.split(',') }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def include
|
|
72
|
+
params[:include]&.split(',')
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
Mime::Type.register('application/vnd.api+json', :json_api)
|
|
2
|
+
|
|
3
|
+
ActionDispatch::Request.parameter_parsers[:json_api] = lambda do |body|
|
|
4
|
+
ActiveSupport::JSON.decode(body)
|
|
5
|
+
end
|
|
6
|
+
|
|
7
|
+
ActiveSupport.on_load(:action_controller) do
|
|
8
|
+
ActionController::Renderers.add(:json_api) do |resources, opts|
|
|
9
|
+
# Renderer proc is evaluated in the controller context.
|
|
10
|
+
self.content_type ||= Mime[:json_api]
|
|
11
|
+
|
|
12
|
+
ActiveSupport::Notifications.instrument('render.json_api', resources: resources, opts: opts) do
|
|
13
|
+
if resources.is_a?(InfinumJsonApiSetup::Error::Base)
|
|
14
|
+
break InfinumJsonApiSetup::JsonApi::ErrorSerializer.new(resources).serialized_json
|
|
15
|
+
end
|
|
16
|
+
break if opts[:status] == 204
|
|
17
|
+
|
|
18
|
+
serializer = opts.delete(:serializer) do
|
|
19
|
+
"#{controller_path.classify.pluralize}::Serializer".constantize
|
|
20
|
+
end
|
|
21
|
+
options = InfinumJsonApiSetup::JsonApi::SerializerOptions.new(
|
|
22
|
+
params: params.to_unsafe_h, pagination_details: opts[:pagination_details]
|
|
23
|
+
).build
|
|
24
|
+
|
|
25
|
+
serializer.new(resources, options.merge(opts)).serializable_hash.to_json
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module Rspec
|
|
3
|
+
module Helpers
|
|
4
|
+
module RequestHelper
|
|
5
|
+
def default_headers
|
|
6
|
+
{
|
|
7
|
+
'content-type' => 'application/vnd.api+json',
|
|
8
|
+
'accept' => 'application/vnd.api+json'
|
|
9
|
+
}
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def json_api_relationships_request_body(relationships)
|
|
13
|
+
json_api_relationship_data(relationships).to_json
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def json_api_request_body(type:, attributes: {}, relationships: {}, included: [], **options)
|
|
17
|
+
{}.tap do |body|
|
|
18
|
+
body[:data] = _data_params(type, attributes, relationships, **options)
|
|
19
|
+
body[:included] = _included_params(included).presence
|
|
20
|
+
end.merge(options).compact.to_json
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def _data_params(type, attributes, relationships, **options)
|
|
24
|
+
attributes = attributes.with_indifferent_access
|
|
25
|
+
|
|
26
|
+
{
|
|
27
|
+
type: type,
|
|
28
|
+
id: attributes.delete(:id).to_s,
|
|
29
|
+
attributes: attributes.presence,
|
|
30
|
+
relationships: _relationships_params(relationships).presence
|
|
31
|
+
}.merge(options).compact
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def _included_params(resources)
|
|
35
|
+
resources.reject do |resource|
|
|
36
|
+
resource[:attributes].blank? && resource[:relationships].blank?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def _relationships_params(relationships)
|
|
41
|
+
relationships.transform_values do |relationship|
|
|
42
|
+
json_api_relationship_data(relationship)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def json_api_relationship_data(relationship)
|
|
47
|
+
{
|
|
48
|
+
data: case relationship
|
|
49
|
+
when Array then relationship.map { |rel| rel.slice(:id, :type) }
|
|
50
|
+
when Hash then relationship.slice(:id, :type)
|
|
51
|
+
when NilClass then nil
|
|
52
|
+
else raise ArgumentError 'relationship must be either an array or hash'
|
|
53
|
+
end
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def json_api_relationship(resource, type: nil)
|
|
58
|
+
if resource.is_a?(Array)
|
|
59
|
+
type = type.presence || _detect_resource_type(resource.first)
|
|
60
|
+
resource.map { |item| json_api_relationship_hash(item.id, type) }
|
|
61
|
+
else
|
|
62
|
+
type = type.presence || _detect_resource_type(resource)
|
|
63
|
+
json_api_relationship_hash(resource.id, type)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def json_api_relationship_hash(id, type)
|
|
68
|
+
{
|
|
69
|
+
id: id.to_s,
|
|
70
|
+
type: type
|
|
71
|
+
}
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def _detect_resource_type(resource)
|
|
75
|
+
resource.class.name.parameterize.pluralize
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module Rspec
|
|
3
|
+
module Helpers
|
|
4
|
+
module ResponseHelper
|
|
5
|
+
def json_response
|
|
6
|
+
JSON.parse(response.body, symbolize_names: true)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def response_item
|
|
10
|
+
raise 'json response is not an item' if json_response[:data].is_a?(Array)
|
|
11
|
+
|
|
12
|
+
OpenStruct.new(json_response[:data][:attributes])
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def response_collection
|
|
16
|
+
raise 'json response is not a collection' unless json_response[:data].is_a?(Array)
|
|
17
|
+
|
|
18
|
+
json_response[:data].map do |item|
|
|
19
|
+
OpenStruct.new(id: item[:id], type: item[:type], **item[:attributes])
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def response_relationships(response_type: :item)
|
|
24
|
+
case response_type
|
|
25
|
+
when :item then json_response.dig(:data, :relationships)
|
|
26
|
+
when :collection then json_response[:data].pluck(:relationships)
|
|
27
|
+
else raise ArgumentError ':response_type must be one of [:item, :collection]'
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def response_meta
|
|
32
|
+
json_response[:meta]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def response_included
|
|
36
|
+
json_response[:included].map do |item|
|
|
37
|
+
OpenStruct.new(id: item[:id], type: item[:type], **item[:attributes])
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def response_included_relationship(name)
|
|
42
|
+
data = response_relationships.fetch(name)[:data]
|
|
43
|
+
|
|
44
|
+
return if data.nil?
|
|
45
|
+
|
|
46
|
+
relationship_id, relationship_type =
|
|
47
|
+
response_relationships.fetch(name)[:data].values_at(:id, :type)
|
|
48
|
+
|
|
49
|
+
response_included.find do |object|
|
|
50
|
+
object.id == relationship_id && object.type == relationship_type
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module RSpec
|
|
3
|
+
module Matchers
|
|
4
|
+
# @return [InfinumJsonApiSetup::Rspec::Matchers::HaveEmptyData]
|
|
5
|
+
def have_empty_data # rubocop:disable Naming/PredicateName
|
|
6
|
+
HaveEmptyData.new
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
class HaveEmptyData < JsonBodyMatcher
|
|
10
|
+
def initialize
|
|
11
|
+
super(Matchers::Util::BodyParser.new('data'))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
attr_reader :data
|
|
17
|
+
|
|
18
|
+
def body_matches?
|
|
19
|
+
data.empty?
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def match_failure_message
|
|
23
|
+
"Expected response data(#{data}) to be empty, but isn't"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def process_parsing_result(data)
|
|
27
|
+
@data = data
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module RSpec
|
|
3
|
+
module Matchers
|
|
4
|
+
# @param [String] pointer
|
|
5
|
+
# @return [InfinumJsonApiSetup::Rspec::Matchers::HaveErrorPointer]
|
|
6
|
+
def have_error_pointer(pointer) # rubocop:disable Naming/PredicateName
|
|
7
|
+
HaveErrorPointer.new(pointer)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class HaveErrorPointer < JsonBodyMatcher
|
|
11
|
+
# @param [String] pointer
|
|
12
|
+
def initialize(pointer)
|
|
13
|
+
super(Matchers::Util::BodyParser.new('errors'))
|
|
14
|
+
|
|
15
|
+
@pointer = pointer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :pointer
|
|
21
|
+
attr_reader :errors
|
|
22
|
+
|
|
23
|
+
def body_matches?
|
|
24
|
+
errors.any? { |error| error.dig('source', 'pointer') == pointer }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def match_failure_message
|
|
28
|
+
"Expected error pointers to include '#{pointer}', but didn't"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def process_parsing_result(result)
|
|
32
|
+
@errors = result
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module RSpec
|
|
3
|
+
module Matchers
|
|
4
|
+
# @param [Integer] expected_count
|
|
5
|
+
# @return [InfinumJsonApiSetup::Rspec::Matchers::HaveResourceCountOf]
|
|
6
|
+
def have_resource_count_of(expected_count) # rubocop:disable Naming/PredicateName
|
|
7
|
+
HaveResourceCountOf.new(expected_count)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class HaveResourceCountOf < JsonBodyMatcher
|
|
11
|
+
# @param [Integer] expected_count
|
|
12
|
+
def initialize(expected_count)
|
|
13
|
+
super(Matchers::Util::BodyParser.new('data'))
|
|
14
|
+
|
|
15
|
+
@expected_count = expected_count
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :data
|
|
21
|
+
attr_reader :expected_count
|
|
22
|
+
|
|
23
|
+
def body_matches?
|
|
24
|
+
data.length == expected_count
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def match_failure_message
|
|
28
|
+
"Expected response data to have #{expected_count} items, but had #{data.length}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def process_parsing_result(data)
|
|
32
|
+
@data = data
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module RSpec
|
|
3
|
+
module Matchers
|
|
4
|
+
# @param [Array<Integer>] required_ids
|
|
5
|
+
# @return [InfinumJsonApiSetup::Rspec::Matchers::IncludeAllResourceIds]
|
|
6
|
+
def include_all_resource_ids(required_ids)
|
|
7
|
+
IncludeAllResourceIds.new(required_ids)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class IncludeAllResourceIds < JsonBodyMatcher
|
|
11
|
+
# @param [Array<Integer>] required_ids
|
|
12
|
+
def initialize(required_ids)
|
|
13
|
+
super(Matchers::Util::BodyParser.new('data'))
|
|
14
|
+
|
|
15
|
+
@required_ids = process_required_ids(required_ids)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :actual_ids
|
|
21
|
+
attr_reader :required_ids
|
|
22
|
+
|
|
23
|
+
def body_matches?
|
|
24
|
+
actual_ids == required_ids
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def match_failure_message
|
|
28
|
+
"Expected response ID's(#{actual_ids}) to match #{required_ids}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def process_parsing_result(data)
|
|
32
|
+
@actual_ids = process_actual_ids(
|
|
33
|
+
data.map { |item| process_actual_id(item['id']) }
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def process_required_ids(ids)
|
|
38
|
+
ids.sort
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def process_actual_id(value)
|
|
42
|
+
value.to_i
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def process_actual_ids(ids)
|
|
46
|
+
ids.sort
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module RSpec
|
|
3
|
+
module Matchers
|
|
4
|
+
# @param [Array<Integer>] required_ids
|
|
5
|
+
# @return [InfinumJsonApiSetup::Rspec::Matchers::IncludeAllResourceIdsSorted]
|
|
6
|
+
def include_all_resource_ids_sorted(required_ids)
|
|
7
|
+
IncludeAllResourceIdsSorted.new(required_ids)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class IncludeAllResourceIdsSorted < IncludeAllResourceIds
|
|
11
|
+
def process_required_ids(ids)
|
|
12
|
+
ids
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def process_actual_ids(ids)
|
|
16
|
+
ids
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module RSpec
|
|
3
|
+
module Matchers
|
|
4
|
+
# @param [Array<Integer>] required_ids
|
|
5
|
+
# @return [InfinumJsonApiSetup::Rspec::Matchers::IncludeAllResourceStringIds]
|
|
6
|
+
def include_all_resource_string_ids(required_ids)
|
|
7
|
+
IncludeAllResourceStringIds.new(required_ids)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class IncludeAllResourceStringIds < IncludeAllResourceIds
|
|
11
|
+
def process_actual_id(id)
|
|
12
|
+
id
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
module InfinumJsonApiSetup
|
|
2
|
+
module RSpec
|
|
3
|
+
module Matchers
|
|
4
|
+
# @param [String] error_detail
|
|
5
|
+
# @return [InfinumJsonApiSetup::Rspec::Matchers::IncludeErrorDetail]
|
|
6
|
+
def include_error_detail(error_detail)
|
|
7
|
+
IncludeErrorDetail.new(error_detail)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
class IncludeErrorDetail < JsonBodyMatcher
|
|
11
|
+
# @param [String] error_detail
|
|
12
|
+
def initialize(error_detail)
|
|
13
|
+
super(Matchers::Util::BodyParser.new('errors'))
|
|
14
|
+
|
|
15
|
+
@error_detail = error_detail
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
attr_reader :error_detail
|
|
21
|
+
attr_reader :errors
|
|
22
|
+
|
|
23
|
+
def body_matches?
|
|
24
|
+
errors.any? { |error| error['detail'].include?(error_detail) }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def match_failure_message
|
|
28
|
+
"Expected error details to include '#{error_detail}', but didn't"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def process_parsing_result(result)
|
|
32
|
+
@errors = result
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|