infinum_json_api_setup 0.0.3

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