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.
Files changed (115) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +6 -0
  3. data/.overcommit.yml +26 -0
  4. data/.rspec +2 -0
  5. data/.rubocop.yml +29 -0
  6. data/.ruby-version +1 -0
  7. data/.simplecov +3 -0
  8. data/Gemfile +10 -0
  9. data/Gemfile.lock +268 -0
  10. data/README.md +156 -0
  11. data/Rakefile +1 -0
  12. data/bin/setup +18 -0
  13. data/infinum_json_api_setup.gemspec +30 -0
  14. data/lib/generators/infinum_json_api_setup/install_generator.rb +12 -0
  15. data/lib/generators/infinum_json_api_setup/templates/config/locales/json_api.en.yml +21 -0
  16. data/lib/infinum_json_api_setup/error.rb +61 -0
  17. data/lib/infinum_json_api_setup/json_api/content_negotiation.rb +29 -0
  18. data/lib/infinum_json_api_setup/json_api/error_handling.rb +49 -0
  19. data/lib/infinum_json_api_setup/json_api/error_serializer.rb +72 -0
  20. data/lib/infinum_json_api_setup/json_api/request_parsing.rb +17 -0
  21. data/lib/infinum_json_api_setup/json_api/responder.rb +31 -0
  22. data/lib/infinum_json_api_setup/json_api/serializer_options.rb +76 -0
  23. data/lib/infinum_json_api_setup/rails.rb +28 -0
  24. data/lib/infinum_json_api_setup/rspec/helpers/request_helper.rb +80 -0
  25. data/lib/infinum_json_api_setup/rspec/helpers/response_helper.rb +56 -0
  26. data/lib/infinum_json_api_setup/rspec/matchers/have_empty_data.rb +32 -0
  27. data/lib/infinum_json_api_setup/rspec/matchers/have_error_pointer.rb +37 -0
  28. data/lib/infinum_json_api_setup/rspec/matchers/have_resource_count_of.rb +37 -0
  29. data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_ids.rb +51 -0
  30. data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_sorted.rb +21 -0
  31. data/lib/infinum_json_api_setup/rspec/matchers/include_all_resource_string_ids.rb +17 -0
  32. data/lib/infinum_json_api_setup/rspec/matchers/include_error_detail.rb +37 -0
  33. data/lib/infinum_json_api_setup/rspec/matchers/include_related_resource.rb +42 -0
  34. data/lib/infinum_json_api_setup/rspec/matchers/json_body_matcher.rb +42 -0
  35. data/lib/infinum_json_api_setup/rspec/matchers/schema_matchers.rb +29 -0
  36. data/lib/infinum_json_api_setup/rspec.rb +21 -0
  37. data/lib/infinum_json_api_setup/version.rb +3 -0
  38. data/lib/infinum_json_api_setup.rb +16 -0
  39. data/spec/controllers/api/v1/base_controller_spec.rb +9 -0
  40. data/spec/dummy/Rakefile +6 -0
  41. data/spec/dummy/app/assets/config/manifest.js +1 -0
  42. data/spec/dummy/app/assets/stylesheets/application.css +15 -0
  43. data/spec/dummy/app/channels/application_cable/channel.rb +4 -0
  44. data/spec/dummy/app/channels/application_cable/connection.rb +4 -0
  45. data/spec/dummy/app/controllers/api/v1/base_controller.rb +11 -0
  46. data/spec/dummy/app/controllers/api/v1/locations_controller.rb +64 -0
  47. data/spec/dummy/app/controllers/application_controller.rb +2 -0
  48. data/spec/dummy/app/controllers/concerns/.keep +0 -0
  49. data/spec/dummy/app/javascript/packs/application.js +15 -0
  50. data/spec/dummy/app/jobs/application_job.rb +7 -0
  51. data/spec/dummy/app/mailers/application_mailer.rb +4 -0
  52. data/spec/dummy/app/models/application_record.rb +3 -0
  53. data/spec/dummy/app/models/concerns/.keep +0 -0
  54. data/spec/dummy/app/models/location.rb +38 -0
  55. data/spec/dummy/app/models/location_label.rb +3 -0
  56. data/spec/dummy/app/policies/application_policy.rb +51 -0
  57. data/spec/dummy/app/views/layouts/mailer.html.erb +13 -0
  58. data/spec/dummy/app/views/layouts/mailer.text.erb +1 -0
  59. data/spec/dummy/app/web/api/v1/locations/label_serializer.rb +13 -0
  60. data/spec/dummy/app/web/api/v1/locations/policy.rb +28 -0
  61. data/spec/dummy/app/web/api/v1/locations/query.rb +12 -0
  62. data/spec/dummy/app/web/api/v1/locations/serializer.rb +15 -0
  63. data/spec/dummy/bin/rails +4 -0
  64. data/spec/dummy/bin/rake +4 -0
  65. data/spec/dummy/bin/setup +33 -0
  66. data/spec/dummy/config/application.rb +39 -0
  67. data/spec/dummy/config/boot.rb +5 -0
  68. data/spec/dummy/config/cable.yml +10 -0
  69. data/spec/dummy/config/database.yml +15 -0
  70. data/spec/dummy/config/environment.rb +5 -0
  71. data/spec/dummy/config/environments/development.rb +65 -0
  72. data/spec/dummy/config/environments/production.rb +114 -0
  73. data/spec/dummy/config/environments/test.rb +60 -0
  74. data/spec/dummy/config/initializers/application_controller_renderer.rb +8 -0
  75. data/spec/dummy/config/initializers/backtrace_silencers.rb +10 -0
  76. data/spec/dummy/config/initializers/cors.rb +16 -0
  77. data/spec/dummy/config/initializers/filter_parameter_logging.rb +6 -0
  78. data/spec/dummy/config/initializers/inflections.rb +16 -0
  79. data/spec/dummy/config/initializers/mime_types.rb +4 -0
  80. data/spec/dummy/config/initializers/wrap_parameters.rb +14 -0
  81. data/spec/dummy/config/locales/en.yml +33 -0
  82. data/spec/dummy/config/locales/json_api.en.yml +21 -0
  83. data/spec/dummy/config/puma.rb +43 -0
  84. data/spec/dummy/config/routes.rb +7 -0
  85. data/spec/dummy/config/storage.yml +34 -0
  86. data/spec/dummy/config.ru +6 -0
  87. data/spec/dummy/db/migrate/20210709100750_create_locations.rb +10 -0
  88. data/spec/dummy/db/migrate/20210714104736_create_location_labels.rb +10 -0
  89. data/spec/dummy/db/schema.rb +34 -0
  90. data/spec/dummy/log/.keep +0 -0
  91. data/spec/dummy/storage/.keep +0 -0
  92. data/spec/factories/location.rb +11 -0
  93. data/spec/factories/location_label.rb +6 -0
  94. data/spec/infinum_json_api_setup/rspec/matchers/have_empty_data_spec.rb +43 -0
  95. data/spec/infinum_json_api_setup/rspec/matchers/have_error_pointer_spec.rb +43 -0
  96. data/spec/infinum_json_api_setup/rspec/matchers/have_resource_count_of_spec.rb +43 -0
  97. data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_sorted_spec.rb +53 -0
  98. data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_ids_spec.rb +49 -0
  99. data/spec/infinum_json_api_setup/rspec/matchers/include_all_resource_string_ids_spec.rb +49 -0
  100. data/spec/infinum_json_api_setup/rspec/matchers/include_error_detail_spec.rb +43 -0
  101. data/spec/infinum_json_api_setup/rspec/matchers/include_related_resource_spec.rb +43 -0
  102. data/spec/infinum_json_api_setup/rspec/matchers/util/body_parser.rb +30 -0
  103. data/spec/rails_helper.rb +77 -0
  104. data/spec/requests/api/v1/content_negotiation_spec.rb +29 -0
  105. data/spec/requests/api/v1/error_handling_spec.rb +114 -0
  106. data/spec/requests/api/v1/responder_spec.rb +91 -0
  107. data/spec/requests/api/v1/serializer_options_spec.rb +46 -0
  108. data/spec/spec_helper.rb +94 -0
  109. data/spec/support/factory_bot.rb +7 -0
  110. data/spec/support/infinum_json_api_setup.rb +1 -0
  111. data/spec/support/rspec_expectations.rb +5 -0
  112. data/spec/support/test_helpers/matchers/response.rb +17 -0
  113. data/spec/support/test_helpers/request.rb +11 -0
  114. data/spec/support/test_helpers/response.rb +8 -0
  115. 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