jpie 3.0.3 → 3.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 74099168434a717c88d4fc6ade7c97b6aa049f31fc597cd0db76e2d816a693a7
4
- data.tar.gz: 70a2b20227fb36989590b8fd35e9bb4a27b6776569373eea9e6a7c2bf53da606
3
+ metadata.gz: 67ba9f4553fe73ffa121c436107bb030327211038e4e96130fabb48d585419b4
4
+ data.tar.gz: 41cd0fa175eb9ddaca6287945aa81b7e2be49349e2db05d1fbfa4bfcf4fc78f1
5
5
  SHA512:
6
- metadata.gz: f2cd53a342faa153971456b1a5024bb2c61b39d6f0e43345e058993c1c7c8a6f8d71b58726f28786ec6230548c2e232b30c64c06f5aae0323ae58c0b27ac7284
7
- data.tar.gz: 35619211ebc1a5455ee003702943b078eba8c3f94a0d11e50a284ae9eab9e9f35da0ca7e5fc464a4cb3923c7ad83075923d36c4b8c2dc86c217bed20f4c6790d
6
+ metadata.gz: 9ad6c4938d4132a0730ac1f2872f66658ffbf23ae3814f923ede8cd63d57b87dfffaab8843c40c3aee1a38b3d524ab4fca1ce15a805e9d4fddc5db7861b26b73
7
+ data.tar.gz: ec1fc23309a91b8e38f9720cedf255f908585d01b952e62ae2b78f87398e13119b50cd0b14507e401f2141e2c28c70beae6b060d7734db7d5621365cf6745e9a
@@ -0,0 +1,27 @@
1
+ name: CI
2
+
3
+ on:
4
+ pull_request:
5
+ push:
6
+ branches: [main]
7
+
8
+ jobs:
9
+ rubocop:
10
+ runs-on: ubuntu-latest
11
+ steps:
12
+ - uses: actions/checkout@v5
13
+ - uses: ruby/setup-ruby@v1
14
+ with:
15
+ ruby-version: "3.4"
16
+ bundler-cache: true
17
+ - run: bundle exec rubocop
18
+
19
+ rspec:
20
+ runs-on: ubuntu-latest
21
+ steps:
22
+ - uses: actions/checkout@v5
23
+ - uses: ruby/setup-ruby@v1
24
+ with:
25
+ ruby-version: "3.4"
26
+ bundler-cache: true
27
+ - run: bundle exec rspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- jpie (3.0.2)
4
+ jpie (3.0.3)
5
5
  actionpack (~> 8.1, >= 8.1.0)
6
6
  pg_query (>= 4)
7
7
  prosopite (>= 1)
@@ -8,6 +8,7 @@ module JSONAPI
8
8
  rescue_from JSONAPI::AuthorizationError, with: :render_jsonapi_authorization_error
9
9
  rescue_from Pundit::NotAuthorizedError, with: :render_jsonapi_authorization_error if defined?(Pundit)
10
10
  rescue_from JSONAPI::Support::Sort::InvalidFieldError, with: :render_invalid_sort_field_error
11
+ rescue_from JSONAPI::Errors::ParameterNotAllowed, with: :render_parameter_not_allowed_error
11
12
 
12
13
  private
13
14
 
@@ -39,9 +39,7 @@ module JSONAPI
39
39
  end
40
40
 
41
41
  def jsonapi_params
42
- data = params.require(:data)
43
- return data if data.is_a?(Array)
44
-
42
+ data = require_query_object(params.require(:data), "data")
45
43
  permitted = data.permit(:type, :id, attributes: {})
46
44
  permitted[:relationships] = permit_relationships(data) if data[:relationships].present?
47
45
  permitted
@@ -64,17 +62,11 @@ module JSONAPI
64
62
  end
65
63
 
66
64
  def jsonapi_type
67
- data = jsonapi_params
68
- return data.first[:type] if data.is_a?(Array)
69
-
70
- data[:type]
65
+ jsonapi_params[:type]
71
66
  end
72
67
 
73
68
  def jsonapi_id
74
- data = jsonapi_params
75
- return data.first[:id].to_s.presence if data.is_a?(Array)
76
-
77
- data[:id].to_s.presence
69
+ jsonapi_params[:id].to_s.presence
78
70
  end
79
71
 
80
72
  def parse_include_param
@@ -86,7 +78,7 @@ module JSONAPI
86
78
  def parse_fields_param
87
79
  return {} unless params[:fields]
88
80
 
89
- params[:fields].permit!.to_h.each_with_object({}) do |(type, fields), hash|
81
+ require_query_object(params[:fields], "fields").permit!.to_h.each_with_object({}) do |(type, fields), hash|
90
82
  hash[type.to_sym] = fields.to_s.split(",").map(&:strip)
91
83
  end
92
84
  end
@@ -94,7 +86,7 @@ module JSONAPI
94
86
  def parse_filter_param
95
87
  return {} unless params[:filter]
96
88
 
97
- raw_filters = params[:filter].permit!.to_h
89
+ raw_filters = require_query_object(params[:filter], "filter").permit!.to_h
98
90
  JSONAPI::ParamHelpers.flatten_filter_hash(raw_filters)
99
91
  end
100
92
 
@@ -107,7 +99,29 @@ module JSONAPI
107
99
  def parse_page_param
108
100
  return {} unless params[:page]
109
101
 
110
- params[:page].permit(:number, :size).to_h
102
+ page = require_query_object(params[:page], "page").permit(:number, :size).to_h
103
+ page.each { |key, value| require_positive_integer(value, "page[#{key}]") }
104
+ page
105
+ end
106
+
107
+ # JSON:API query family params (filter/page/fields) are objects. A client can
108
+ # instead send a scalar (?filter=x) or array (?filter[]=x), which Rails parses
109
+ # as a String/Array that does not respond to permit!/permit. Reject those as a
110
+ # bad request rather than letting the NoMethodError escape as a 500.
111
+ def require_query_object(value, name)
112
+ return value if value.is_a?(ActionController::Parameters)
113
+
114
+ raise JSONAPI::Errors::ParameterNotAllowed, ["#{name} must be a JSON object"]
115
+ end
116
+
117
+ # page[number]/page[size] are coerced with to_i and used as a divisor and as
118
+ # LIMIT/OFFSET. A non-positive or non-numeric value (0, -5, "abc") produces a
119
+ # divide-by-zero (FloatDomainError) or a negative LIMIT/OFFSET. Reject it as a
120
+ # bad request instead of letting the raise escape as a 500.
121
+ def require_positive_integer(value, name)
122
+ return if value.to_s.match?(/\A\d+\z/) && value.to_i.positive?
123
+
124
+ raise JSONAPI::Errors::ParameterNotAllowed, ["#{name} must be a positive integer"]
111
125
  end
112
126
 
113
127
  def render_sort_errors(invalid)
@@ -69,6 +69,25 @@ module JSONAPI
69
69
  type
70
70
  end
71
71
 
72
+ # jpie routes a relationship to the to-one or to-many path based on the shape of
73
+ # the incoming data, not the association definition. A to-one association given an
74
+ # array (or a to-many given a single identifier) lands on the wrong setter
75
+ # (e.g. user_ids= for a belongs_to) and raises UnknownAttributeError. Reject the
76
+ # mismatch as a client error instead of letting it escape as a 500. AS attachments
77
+ # have no plain reflection and are validated separately, so they are skipped here.
78
+ def validate_relationship_arity!(association_name, data)
79
+ association = @model_class&.reflect_on_association(association_name.to_sym)
80
+ return unless association
81
+
82
+ if association.collection? && !data.is_a?(Array)
83
+ raise ArgumentError,
84
+ "Relationship #{association_name} is to-many and expects an array of resource identifiers"
85
+ elsif !association.collection? && data.is_a?(Array)
86
+ raise ArgumentError,
87
+ "Relationship #{association_name} is to-one and expects a single resource identifier, not an array"
88
+ end
89
+ end
90
+
72
91
  def valid_relationship_data?(data)
73
92
  if data.is_a?(Array)
74
93
  data.all? { |r| valid_resource_identifier?(r) }
@@ -14,6 +14,7 @@ module JSONAPI
14
14
  return handle_empty_array_relationship(attrs, param_name, association_name) if ParamHelpers.empty_array?(data)
15
15
 
16
16
  validate_relationship_data_format!(data, association_name)
17
+ validate_relationship_arity!(association_name, data)
17
18
  process_relationship_data(attrs, association_name, param_name, data)
18
19
  end
19
20
 
@@ -27,7 +27,7 @@ module JSONAPI
27
27
  return scope unless model_class.respond_to?(filter_name.to_sym)
28
28
 
29
29
  scope.public_send(filter_name.to_sym, filter_value)
30
- rescue ArgumentError, NoMethodError
30
+ rescue ArgumentError, NoMethodError, TypeError
31
31
  scope
32
32
  end
33
33
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JSONAPI
4
- VERSION = "3.0.3"
4
+ VERSION = "3.1.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jpie
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.0.3
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emil Kampp
@@ -93,10 +93,10 @@ files:
93
93
  - ".cursor/agents/nasa-power-of-ten-reviewer.md"
94
94
  - ".cursor/agents/systematic-debugging.md"
95
95
  - ".cursor/rules/release.mdc"
96
+ - ".github/workflows/ci.yml"
96
97
  - ".gitignore"
97
98
  - ".rspec"
98
99
  - ".rubocop.yml"
99
- - ".travis.yml"
100
100
  - Gemfile
101
101
  - Gemfile.lock
102
102
  - PERFORMANCE_BASELINE.md
data/.travis.yml DELETED
@@ -1,7 +0,0 @@
1
- ---
2
- sudo: false
3
- language: ruby
4
- cache: bundler
5
- rvm:
6
- - 2.6.10
7
- before_install: gem install bundler -v 1.17.2