infinum_json_api_setup 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|