json_apiable 0.2.0 → 0.5

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 02ceaeecfcdb306933da14c8f4596e355958a6bd6902dc37a365295e409d085e
4
- data.tar.gz: b5dc5338fcb3cd7fd1991f4dc18871ffdc3ed4d63c99d0e826469b558dba8dc0
3
+ metadata.gz: 3843cfd61c5ec39658230e3aee84aac39ff18a26799a881c1d872a3b47872198
4
+ data.tar.gz: 8e4296ea01ed83998c2fd306d720c180bc78af3e3c379668244a833033cc34be
5
5
  SHA512:
6
- metadata.gz: 5c5ddc873cee6e321ab133f8422d22dfe3cc2c46ae57af5c2a1bbfc9b759e00c50b333e11af758ef80f89c8f1b1a5edcbbbd8edb2dd79c13f4659ae20e8a5d4a
7
- data.tar.gz: 8e9cbdf0d269a0682b2f863b50fdcc71ed00a991174be19d293d75463578cada37390254d45d7a389b6447ece782d6c922b9bef79bc8d9518ede3bbbbf24188f
6
+ metadata.gz: 7c96a6ab06c100116d3d9317d39702dda617131e11b74abfa53f120911c2aa221612af7d9ced0be283a142da56dbf84966a07de36915267500fe797f07753b57
7
+ data.tar.gz: 953edf80dfe8550142e09da95ad3f1c8bc6502b2fede6dfb0afd9169aaccc40e75d0c96e9e59bf879fdac6f0de80a0c73ccbc3a133e26d84b98441ba267171c3
data/.rubocop.yml ADDED
@@ -0,0 +1,39 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.4
3
+ Exclude:
4
+ - 'db/**/*'
5
+ - 'bin/**/*'
6
+ - 'config/**/*'
7
+ - 'spec/**/*'
8
+ - 'json_apiable.gemspec'
9
+ - 'Gemfile'
10
+ - 'Rakefile'
11
+
12
+ Metrics:
13
+ Enabled: false
14
+
15
+ Naming/AccessorMethodName:
16
+ Enabled: false
17
+
18
+ Style/PercentLiteralDelimiters:
19
+ Enabled: false
20
+
21
+ Style/HashSyntax:
22
+ Enabled: false
23
+
24
+ Style/Documentation:
25
+ Enabled: false
26
+
27
+ Style/StringLiterals:
28
+ Enabled: false
29
+
30
+ Style/ClassAndModuleChildren:
31
+ Enabled: false
32
+
33
+ Style/SymbolArray:
34
+ Enabled: false
35
+
36
+ Layout/DotPosition:
37
+ EnforcedStyle: trailing
38
+
39
+
data/Gemfile.lock CHANGED
@@ -1,127 +1,131 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- json_apiable (0.2.0)
5
- activerecord (>= 4.2)
6
- activesupport (>= 4.2)
7
- fast_jsonapi (~> 1.5)
4
+ json_apiable (0.5)
5
+ activerecord
6
+ activesupport
7
+ jsonapi-serializer
8
8
 
9
9
  GEM
10
10
  remote: https://rubygems.org/
11
11
  specs:
12
- actioncable (6.0.2.1)
13
- actionpack (= 6.0.2.1)
12
+ actioncable (6.1.3.1)
13
+ actionpack (= 6.1.3.1)
14
+ activesupport (= 6.1.3.1)
14
15
  nio4r (~> 2.0)
15
16
  websocket-driver (>= 0.6.1)
16
- actionmailbox (6.0.2.1)
17
- actionpack (= 6.0.2.1)
18
- activejob (= 6.0.2.1)
19
- activerecord (= 6.0.2.1)
20
- activestorage (= 6.0.2.1)
21
- activesupport (= 6.0.2.1)
17
+ actionmailbox (6.1.3.1)
18
+ actionpack (= 6.1.3.1)
19
+ activejob (= 6.1.3.1)
20
+ activerecord (= 6.1.3.1)
21
+ activestorage (= 6.1.3.1)
22
+ activesupport (= 6.1.3.1)
22
23
  mail (>= 2.7.1)
23
- actionmailer (6.0.2.1)
24
- actionpack (= 6.0.2.1)
25
- actionview (= 6.0.2.1)
26
- activejob (= 6.0.2.1)
24
+ actionmailer (6.1.3.1)
25
+ actionpack (= 6.1.3.1)
26
+ actionview (= 6.1.3.1)
27
+ activejob (= 6.1.3.1)
28
+ activesupport (= 6.1.3.1)
27
29
  mail (~> 2.5, >= 2.5.4)
28
30
  rails-dom-testing (~> 2.0)
29
- actionpack (6.0.2.1)
30
- actionview (= 6.0.2.1)
31
- activesupport (= 6.0.2.1)
32
- rack (~> 2.0, >= 2.0.8)
31
+ actionpack (6.1.3.1)
32
+ actionview (= 6.1.3.1)
33
+ activesupport (= 6.1.3.1)
34
+ rack (~> 2.0, >= 2.0.9)
33
35
  rack-test (>= 0.6.3)
34
36
  rails-dom-testing (~> 2.0)
35
37
  rails-html-sanitizer (~> 1.0, >= 1.2.0)
36
- actiontext (6.0.2.1)
37
- actionpack (= 6.0.2.1)
38
- activerecord (= 6.0.2.1)
39
- activestorage (= 6.0.2.1)
40
- activesupport (= 6.0.2.1)
38
+ actiontext (6.1.3.1)
39
+ actionpack (= 6.1.3.1)
40
+ activerecord (= 6.1.3.1)
41
+ activestorage (= 6.1.3.1)
42
+ activesupport (= 6.1.3.1)
41
43
  nokogiri (>= 1.8.5)
42
- actionview (6.0.2.1)
43
- activesupport (= 6.0.2.1)
44
+ actionview (6.1.3.1)
45
+ activesupport (= 6.1.3.1)
44
46
  builder (~> 3.1)
45
47
  erubi (~> 1.4)
46
48
  rails-dom-testing (~> 2.0)
47
49
  rails-html-sanitizer (~> 1.1, >= 1.2.0)
48
- activejob (6.0.2.1)
49
- activesupport (= 6.0.2.1)
50
+ activejob (6.1.3.1)
51
+ activesupport (= 6.1.3.1)
50
52
  globalid (>= 0.3.6)
51
- activemodel (6.0.2.1)
52
- activesupport (= 6.0.2.1)
53
- activerecord (6.0.2.1)
54
- activemodel (= 6.0.2.1)
55
- activesupport (= 6.0.2.1)
56
- activestorage (6.0.2.1)
57
- actionpack (= 6.0.2.1)
58
- activejob (= 6.0.2.1)
59
- activerecord (= 6.0.2.1)
60
- marcel (~> 0.3.1)
61
- activesupport (6.0.2.1)
53
+ activemodel (6.1.3.1)
54
+ activesupport (= 6.1.3.1)
55
+ activerecord (6.1.3.1)
56
+ activemodel (= 6.1.3.1)
57
+ activesupport (= 6.1.3.1)
58
+ activestorage (6.1.3.1)
59
+ actionpack (= 6.1.3.1)
60
+ activejob (= 6.1.3.1)
61
+ activerecord (= 6.1.3.1)
62
+ activesupport (= 6.1.3.1)
63
+ marcel (~> 1.0.0)
64
+ mini_mime (~> 1.0.2)
65
+ activesupport (6.1.3.1)
62
66
  concurrent-ruby (~> 1.0, >= 1.0.2)
63
- i18n (>= 0.7, < 2)
64
- minitest (~> 5.1)
65
- tzinfo (~> 1.1)
66
- zeitwerk (~> 2.2)
67
+ i18n (>= 1.6, < 2)
68
+ minitest (>= 5.1)
69
+ tzinfo (~> 2.0)
70
+ zeitwerk (~> 2.3)
67
71
  builder (3.2.4)
68
- byebug (11.0.1)
72
+ byebug (11.1.1)
69
73
  coderay (1.1.2)
70
- concurrent-ruby (1.1.5)
71
- crass (1.0.5)
74
+ concurrent-ruby (1.1.8)
75
+ crass (1.0.6)
72
76
  diff-lcs (1.3)
73
- erubi (1.9.0)
74
- factory_bot (5.1.1)
75
- activesupport (>= 4.2.0)
76
- factory_bot_rails (5.1.1)
77
- factory_bot (~> 5.1.0)
78
- railties (>= 4.2.0)
79
- faker (2.9.0)
80
- i18n (>= 1.6, < 1.8)
81
- fast_jsonapi (1.5)
82
- activesupport (>= 4.2)
77
+ erubi (1.10.0)
78
+ factory_bot (6.1.0)
79
+ activesupport (>= 5.0.0)
80
+ factory_bot_rails (6.1.0)
81
+ factory_bot (~> 6.1.0)
82
+ railties (>= 5.0.0)
83
+ faker (2.10.2)
84
+ i18n (>= 1.6, < 2)
83
85
  globalid (0.4.2)
84
86
  activesupport (>= 4.2.0)
85
- i18n (1.7.0)
87
+ i18n (1.8.10)
86
88
  concurrent-ruby (~> 1.0)
87
- loofah (2.4.0)
89
+ jsonapi-serializer (2.2.0)
90
+ activesupport (>= 4.2)
91
+ loofah (2.9.1)
88
92
  crass (~> 1.0.2)
89
93
  nokogiri (>= 1.5.9)
90
94
  mail (2.7.1)
91
95
  mini_mime (>= 0.1.1)
92
- marcel (0.3.3)
93
- mimemagic (~> 0.3.2)
96
+ marcel (1.0.1)
94
97
  method_source (0.9.2)
95
- mimemagic (0.3.3)
96
- mini_mime (1.0.2)
97
- mini_portile2 (2.4.0)
98
- minitest (5.13.0)
99
- nio4r (2.5.2)
100
- nokogiri (1.10.7)
101
- mini_portile2 (~> 2.4.0)
98
+ mini_mime (1.0.3)
99
+ mini_portile2 (2.5.1)
100
+ minitest (5.14.4)
101
+ nio4r (2.5.7)
102
+ nokogiri (1.11.3)
103
+ mini_portile2 (~> 2.5.0)
104
+ racc (~> 1.4)
102
105
  pry (0.12.2)
103
106
  coderay (~> 1.1.0)
104
107
  method_source (~> 0.9.0)
105
- pry-byebug (3.7.0)
108
+ pry-byebug (3.8.0)
106
109
  byebug (~> 11.0)
107
110
  pry (~> 0.10)
108
- rack (2.0.8)
111
+ racc (1.5.2)
112
+ rack (2.2.3)
109
113
  rack-test (1.1.0)
110
114
  rack (>= 1.0, < 3)
111
- rails (6.0.2.1)
112
- actioncable (= 6.0.2.1)
113
- actionmailbox (= 6.0.2.1)
114
- actionmailer (= 6.0.2.1)
115
- actionpack (= 6.0.2.1)
116
- actiontext (= 6.0.2.1)
117
- actionview (= 6.0.2.1)
118
- activejob (= 6.0.2.1)
119
- activemodel (= 6.0.2.1)
120
- activerecord (= 6.0.2.1)
121
- activestorage (= 6.0.2.1)
122
- activesupport (= 6.0.2.1)
123
- bundler (>= 1.3.0)
124
- railties (= 6.0.2.1)
115
+ rails (6.1.3.1)
116
+ actioncable (= 6.1.3.1)
117
+ actionmailbox (= 6.1.3.1)
118
+ actionmailer (= 6.1.3.1)
119
+ actionpack (= 6.1.3.1)
120
+ actiontext (= 6.1.3.1)
121
+ actionview (= 6.1.3.1)
122
+ activejob (= 6.1.3.1)
123
+ activemodel (= 6.1.3.1)
124
+ activerecord (= 6.1.3.1)
125
+ activestorage (= 6.1.3.1)
126
+ activesupport (= 6.1.3.1)
127
+ bundler (>= 1.15.0)
128
+ railties (= 6.1.3.1)
125
129
  sprockets-rails (>= 2.0.0)
126
130
  rails-controller-testing (1.0.4)
127
131
  actionpack (>= 5.0.1.x)
@@ -132,19 +136,19 @@ GEM
132
136
  nokogiri (>= 1.6)
133
137
  rails-html-sanitizer (1.3.0)
134
138
  loofah (~> 2.3)
135
- railties (6.0.2.1)
136
- actionpack (= 6.0.2.1)
137
- activesupport (= 6.0.2.1)
139
+ railties (6.1.3.1)
140
+ actionpack (= 6.1.3.1)
141
+ activesupport (= 6.1.3.1)
138
142
  method_source
139
143
  rake (>= 0.8.7)
140
- thor (>= 0.20.3, < 2.0)
141
- rake (10.5.0)
142
- rspec-core (3.9.0)
143
- rspec-support (~> 3.9.0)
144
+ thor (~> 1.0)
145
+ rake (13.0.3)
146
+ rspec-core (3.9.1)
147
+ rspec-support (~> 3.9.1)
144
148
  rspec-expectations (3.9.0)
145
149
  diff-lcs (>= 1.2.0, < 2.0)
146
150
  rspec-support (~> 3.9.0)
147
- rspec-mocks (3.9.0)
151
+ rspec-mocks (3.9.1)
148
152
  diff-lcs (>= 1.2.0, < 2.0)
149
153
  rspec-support (~> 3.9.0)
150
154
  rspec-rails (3.9.0)
@@ -155,23 +159,22 @@ GEM
155
159
  rspec-expectations (~> 3.9.0)
156
160
  rspec-mocks (~> 3.9.0)
157
161
  rspec-support (~> 3.9.0)
158
- rspec-support (3.9.0)
159
- sprockets (4.0.0)
162
+ rspec-support (3.9.2)
163
+ sprockets (4.0.2)
160
164
  concurrent-ruby (~> 1.0)
161
165
  rack (> 1, < 3)
162
- sprockets-rails (3.2.1)
166
+ sprockets-rails (3.2.2)
163
167
  actionpack (>= 4.0)
164
168
  activesupport (>= 4.0)
165
169
  sprockets (>= 3.0.0)
166
170
  sqlite3 (1.4.2)
167
- thor (1.0.1)
168
- thread_safe (0.3.6)
169
- tzinfo (1.2.5)
170
- thread_safe (~> 0.1)
171
- websocket-driver (0.7.1)
171
+ thor (1.1.0)
172
+ tzinfo (2.0.4)
173
+ concurrent-ruby (~> 1.0)
174
+ websocket-driver (0.7.3)
172
175
  websocket-extensions (>= 0.1.0)
173
- websocket-extensions (0.1.4)
174
- zeitwerk (2.2.2)
176
+ websocket-extensions (0.1.5)
177
+ zeitwerk (2.4.2)
175
178
 
176
179
  PLATFORMS
177
180
  ruby
@@ -185,9 +188,9 @@ DEPENDENCIES
185
188
  pry-byebug
186
189
  rails
187
190
  rails-controller-testing
188
- rake (~> 10.0)
191
+ rake (~> 13.0)
189
192
  rspec-rails (~> 3.9)
190
193
  sqlite3
191
194
 
192
195
  BUNDLED WITH
193
- 2.1.1
196
+ 2.1.4
data/README.md CHANGED
@@ -1,4 +1,6 @@
1
1
  # JsonApiable
2
+ [![Maintainability](https://api.codeclimate.com/v1/badges/add92f51e18446e44b29/maintainability)](https://codeclimate.com/github/mikemarsian/json_apiable/maintainability)
3
+ [![Gem Version](https://badge.fury.io/rb/json_apiable.svg)](https://badge.fury.io/rb/json_apiable)
2
4
 
3
5
  JsonApiable is a Ruby module that makes it easier for Rails API controllers to handle JSON:API parameter and relationship parsing,
4
6
  strong parameter validation, returning well-structured errors and more - all in a Rails-friendly way.
@@ -54,7 +56,7 @@ class API::PostsController < API::BaseController
54
56
  # GET /v1/posts
55
57
  def index
56
58
  # pass page and include info to your logic
57
- posts = GetPostsService.call(jsonapi_page, jsonapi_include)
59
+ posts = GetPostsService.call(jsonapi_page_hash, jsonapi_include_array)
58
60
  # some other gem, such as fast_jsonapi is assumed to produce the json:api output
59
61
  render json: posts
60
62
  end
@@ -97,21 +99,21 @@ class API::PostsController < API::BaseController
97
99
  #
98
100
  # }
99
101
  @post.update_attributes!(jsonapi_assign_params)
100
- render json: @user
102
+ render json: @post
101
103
  end
102
104
 
103
105
  def create
104
106
  # use jsonapi_attribute_present? to quickly test presence of specific attributes
105
107
  raise UnprocessableEntityError, 'No title!' unless jsonapi_attribute_present?(:title)
106
- # use jsonapi_attribute_value to get attribute values. If non-existent, nil would be returned
107
- @title = jsonapi_attribute_value(:title)
108
+ # use jsonapi_attribute to get attribute values. If non-existent, nil would be returned
109
+ @title = jsonapi_attribute(:title)
108
110
  # exclude 'author' attribute from assign params, for example because it's a separate table on the DB level)
109
111
  @author_name = jsonapi_exclude_attribute(:author_name)
110
112
  # exclude 'comments' relationship from assign params, for example because we want to filter which ones are added to post
111
113
  @comments_hash = jsonapi_exclude_relationship(:comments)
112
114
  do_some_logc_with_excluded_params
113
115
  # jsonapi_assign_params wouldn't include 'author' attribute and 'comments' relationship
114
- User.create!(jsonapi_assign_params)
116
+ Post.create!(jsonapi_assign_params)
115
117
  end
116
118
 
117
119
  protected
@@ -132,6 +134,76 @@ class API::PostsController < API::BaseController
132
134
 
133
135
  end
134
136
  ````
137
+ ### Filters
138
+ JsonApiable supports parsing filter requests in the form `example.com/v1/posts?filter[status]=draft` and returning errors
139
+ in case provided filter keys or values do not adhere to what you define:
140
+
141
+ ```ruby
142
+ # Create filter class that inherits from JsonApiable::BaseFilter
143
+ class API::PostFilter < JsonApiable::BaseFilter
144
+ # Declare which filter keys are supported
145
+ def self.jsonapi_allowed_filters
146
+ {
147
+ # For each key, declare what values are allowed. The supported value matchers include:
148
+ # 1) Array of values
149
+ # example.com/v1/posts?filter[status]=draft,published
150
+ status: Post.statuses.keys,
151
+
152
+ # 2) DateTime matcher - proc that checks that the provided value is a valid DateTime
153
+ # example.com/v1/posts?filter[published_at]='2001-02-03T04:05:06+03:00'
154
+ published_at: datetime_matcher,
155
+
156
+ # 3) Boolean matcher - proc that checks that the provided value is a boolean (true/t/1 for True, false/f/0 for False)
157
+ # example.com/v1/posts?filter[subscribers_only]=true
158
+ subscribers_only: boolean_matcher,
159
+
160
+ # 4) ID matcher - proc that checks that the provided ids exist for given model
161
+ # example.com/v1/posts?filter[ids]=10893,14596
162
+ ids: ids_matcher(Post),
163
+
164
+ # Of course, you can also implement your own matchers. For example:
165
+ reviewed_at: recent_datetime_matcher
166
+ }
167
+ end
168
+
169
+ # Example of custom filter value matcher
170
+ def self.recent_datetime_matcher
171
+ proc do |value|
172
+ datetime = Time.zone.parse(value)
173
+ datetime.present? && datetime > 10.years.ago && datetime < 2.years.from_now
174
+ end
175
+ end
176
+ end
177
+ ```
178
+ Now set the filter for actions which should support filtering:
179
+ ```ruby
180
+ class API::PostsController < API::BaseController
181
+ before_action -> { set_jsonapi_filter(API::PostFilter) }, only: %i[index search]
182
+ end
183
+
184
+ ```
185
+ And you are good to go!
186
+
187
+ Incidentally, PostFilter class is also a good place to implement your filter logic:
188
+ ```ruby
189
+ class API::PostFilter < JsonApiable::BaseFilter
190
+ # The following methods are available to a filter class instance:
191
+ # jsonapi_collection - collection on which to execute filtering
192
+ # jsonapi_filter_hash - a filter query hash, e.g. { 'status' => ['draft', 'published'], 'published_at' => '2001-02-03T04:05:06+03:00' }
193
+ def call
194
+ jsonapi_collection.where(status: jsonapi_filter_hash[:status])
195
+ end
196
+ end
197
+ ```
198
+ Now you can call filter posts collection in your controller:
199
+ ```ruby
200
+ posts = GetPosts.call
201
+ # jsonapi_filter_class - API::PostFilter in our example
202
+ # jsonapi_filter_hash - a filter query hash, e.g. { 'status' => ['draft', 'published'] }
203
+ filtered_posts = jsonapi_filter_class.new(posts, jsonapi_filter_hash).call
204
+ ```
205
+
206
+
135
207
  ### Configuration
136
208
  Add an initializer to your app with the following config block:
137
209
  ```ruby
@@ -150,10 +222,14 @@ JsonApiable.configure do |config|
150
222
  end
151
223
  ```
152
224
 
225
+ ### Gotchas
226
+ - To make sure requests with invalid attributes/relationships result in a well-structured json-api error, configure your Rails app to raise
227
+ exceptions on invalid parameters (JsonApiable will catch them and return an appropriate response). In `config/application.rb` set `config.action_controller.action_on_unpermitted_parameters = :raise`
228
+
153
229
  ### Limitations
154
- - `has_one` associations are currently not supported by `jsonapi_assign_params`. So if the updated resource
155
- contains an association whose foreign key exists in
156
- - complex attributes currently don't work
230
+ - `has_one` associations are expected to be represented as complex-attributes on the API level. So if User `has_one` Address,
231
+ than on the API level, JsonApiable expects address to be specified as a hash inside User's `attributes` rather than a separate relationship.
232
+ This makes sense in most cases. If your API represantion differs, `@post.update_attributes!(jsonapi_assign_params)` assignment won't work correctly.
157
233
 
158
234
  ## Development
159
235
 
data/json_apiable.gemspec CHANGED
@@ -24,9 +24,9 @@ Gem::Specification.new do |spec|
24
24
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
25
  spec.require_paths = ["lib"]
26
26
 
27
- spec.add_dependency 'activerecord', '>= 4.2'
28
- spec.add_dependency 'activesupport', '>= 4.2'
29
- spec.add_dependency 'fast_jsonapi', '~> 1.5'
27
+ spec.add_dependency 'activerecord'
28
+ spec.add_dependency 'activesupport'
29
+ spec.add_dependency 'jsonapi-serializer'
30
30
 
31
31
  spec.add_development_dependency 'bundler', '~> 2.0'
32
32
  spec.add_development_dependency 'factory_bot_rails'
@@ -35,7 +35,7 @@ Gem::Specification.new do |spec|
35
35
  spec.add_development_dependency 'pry-byebug'
36
36
  spec.add_development_dependency 'rails'
37
37
  spec.add_development_dependency 'rails-controller-testing'
38
- spec.add_development_dependency 'rake', '~> 10.0'
38
+ spec.add_development_dependency 'rake', '~> 13.0'
39
39
  spec.add_development_dependency 'rspec-rails', '~> 3.9'
40
40
  spec.add_development_dependency 'sqlite3'
41
41
  end
data/lib/json_apiable.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/all'
2
4
  require "json_apiable/version"
3
5
  require "json_apiable/core_extensions"
@@ -6,7 +8,10 @@ require 'json_apiable/renderers'
6
8
  require 'json_apiable/errors'
7
9
  require 'json_apiable/params_parser'
8
10
  require 'json_apiable/pagination_parser'
11
+ require 'json_apiable/filter_parser'
12
+ require 'json_apiable/filter_matchers'
13
+ require 'json_apiable/base_filter'
9
14
  require 'json_apiable/json_apiable'
10
15
 
11
16
  String.include CoreExtensions::String
12
- Mime::Type.register JsonApiable::JSONAPI_CONTENT_TYPE, :json_api
17
+ Mime::Type.register JsonApiable::JSONAPI_CONTENT_TYPE, :json_api
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonApiable
4
+ # Base class for Filters
5
+ class BaseFilter
6
+ extend JsonApiable::FilterMatchers
7
+
8
+ attr_reader :jsonapi_collection, :jsonapi_filter_hash, :current_user
9
+
10
+ protected
11
+
12
+ def initialize(a_collection, a_filter_hash, current_user)
13
+ @jsonapi_collection = a_collection
14
+ @jsonapi_filter_hash = a_filter_hash
15
+ @current_user = current_user
16
+ end
17
+
18
+ class << self
19
+ def jsonapi_allowed_filters
20
+ {}
21
+ end
22
+ end
23
+ end
24
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
4
  class Configuration
3
5
  attr_accessor :valid_query_params, :supported_media_type_proc, :not_found_exception_class, :page_size
@@ -29,4 +31,4 @@ module JsonApiable
29
31
  @not_found_exception_class = klass
30
32
  end
31
33
  end
32
- end
34
+ end
@@ -1,7 +1,9 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module CoreExtensions
2
4
  module String
3
5
  def integer?
4
6
  to_i.to_s == self
5
7
  end
6
8
  end
7
- end
9
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
4
  module Errors
3
5
  class ApiError < StandardError; end
@@ -7,4 +9,4 @@ module JsonApiable
7
9
  class ForbiddenError < ApiError; end
8
10
  class ConfigurationError < ApiError; end
9
11
  end
10
- end
12
+ end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonApiable
4
+ module FilterMatchers
5
+ def matches?(allowed, given)
6
+ given.all? do |value|
7
+ allowed.is_a?(Proc) ? allowed.call(value) : allowed.include?(value)
8
+ end
9
+ end
10
+
11
+ module_function :matches?
12
+
13
+ # returns true for boolean values, false for any other
14
+ def boolean_matcher
15
+ proc do |value|
16
+ handle_error(value) do
17
+ if true_matcher.call(value) || (value == false || value =~ /^(false|f|0)$/i)
18
+ true
19
+ else
20
+ false
21
+ end
22
+ end
23
+ end
24
+ end
25
+
26
+ # returns true for true values, false for any other
27
+ def true_matcher
28
+ proc do |value|
29
+ handle_error(value) do
30
+ if value == true || value =~ /^(true|t|1)$/i
31
+ true
32
+ else
33
+ false
34
+ end
35
+ end
36
+ end
37
+ end
38
+
39
+ # returns true if the value is a valid date or datetime
40
+ def datetime_matcher
41
+ proc do |value|
42
+ handle_error(value) do
43
+ datetime = value.in_time_zone(Time.zone)
44
+ datetime.present?
45
+ end
46
+ end
47
+ end
48
+
49
+ # returns true if the value is a an array of existing ids of the given model
50
+ def ids_matcher(model)
51
+ proc do |value|
52
+ handle_error(value) do
53
+ given_ids = value.split(',')
54
+ found_records = model.where(id: given_ids)
55
+
56
+ given_ids.count == found_records.count
57
+ end
58
+ end
59
+ end
60
+
61
+ def handle_error(value)
62
+ yield
63
+ rescue ArgumentError => e
64
+ raise ArgumentError, "#{value}: #{e.message}"
65
+ end
66
+ end
67
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JsonApiable
4
+ class FilterParser
5
+ def self.parse_filters!(jsonapi_build_params, filter_class)
6
+ FilterParser.new(jsonapi_build_params[:filter], filter_class).parse!
7
+ end
8
+
9
+ attr_reader :filter_param, :filter_class
10
+
11
+ def initialize(filter_param, filter_class)
12
+ @filter_param = filter_param
13
+ @filter_class = filter_class
14
+ end
15
+
16
+ # Support filtering in the form of example.com/v1/posts?filter[status]=draft,published
17
+ def parse!
18
+ raise_invalid_filter_class unless valid_filter_class?
19
+
20
+ filter_hash = ActiveSupport::HashWithIndifferentAccess.new
21
+ if valid_filter_query?
22
+ filter_param.keys.each do |k|
23
+ if valid_filter_key?(k)
24
+ # support notation ?filter[param]=value1,value2,value3&...
25
+ requested = filter_param[k].split(',')
26
+ allowed_values = allowed_filter_keys[k]
27
+ raise_invalid_filter_value(k) unless FilterMatchers.matches?(allowed_values, requested)
28
+
29
+ filter_hash[k] = requested
30
+ else
31
+ raise_invalid_filter_value(k)
32
+ end
33
+ end
34
+ elsif filter_param.present?
35
+ raise ArgumentError, 'filter'
36
+ end
37
+ filter_hash
38
+ end
39
+
40
+ private
41
+
42
+ def raise_argument_error(message)
43
+ raise ArgumentError, message
44
+ end
45
+
46
+ def raise_invalid_filter_value(k)
47
+ prefix = "filter[#{k}]"
48
+ msg = filter_param[k].present? ? "#{prefix}=#{filter_param[k]}" : prefix
49
+ raise_argument_error(msg)
50
+ end
51
+
52
+ def raise_invalid_filter_class
53
+ raise_argument_error("#{filter_class} does not specify jsonapi_allowed_filters")
54
+ end
55
+
56
+ def valid_filter_class?
57
+ filter_class.respond_to?(:jsonapi_allowed_filters)
58
+ end
59
+
60
+ def valid_filter_query?
61
+ filter_param.present? && (filter_param.is_a?(Hash) || filter_param.is_a?(ActionController::Parameters))
62
+ end
63
+
64
+ def valid_filter_key?(k)
65
+ allowed_filter_keys.key?(k) && filter_param[k].present?
66
+ end
67
+
68
+ def allowed_filter_keys
69
+ filter_class.jsonapi_allowed_filters.with_indifferent_access
70
+ end
71
+ end
72
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
4
  extend ActiveSupport::Concern
3
5
  include Errors
@@ -5,8 +7,8 @@ module JsonApiable
5
7
 
6
8
  JSONAPI_CONTENT_TYPE = 'application/vnd.api+json'
7
9
 
8
- attr_reader :jsonapi_page, :jsonapi_include, :jsonapi_build_params, :jsonapi_assign_params, :jsonapi_default_page_size,
9
- :jsonapi_exclude_attributes, :jsonapi_exclude_relationships
10
+ attr_reader :jsonapi_page_hash, :jsonapi_include_array, :jsonapi_filter_hash, :jsonapi_filter_class, :jsonapi_build_params,
11
+ :jsonapi_assign_params, :jsonapi_default_page_size, :jsonapi_exclude_attributes, :jsonapi_exclude_relationships
10
12
 
11
13
  included do
12
14
  before_action :ensure_jsonapi_content_type
@@ -25,7 +27,6 @@ module JsonApiable
25
27
  rescue_from JsonApiable.configuration.not_found_exception_class, with: :respond_to_not_found
26
28
  end
27
29
 
28
-
29
30
  class << self
30
31
  attr_writer :configuration
31
32
  end
@@ -42,36 +43,63 @@ module JsonApiable
42
43
  yield(configuration)
43
44
  end
44
45
 
46
+ def jsonapi_attribute(attrib_key)
47
+ jsonapi_build_params.dig(:data, :attributes, attrib_key)
48
+ end
49
+
45
50
  def jsonapi_attribute_present?(attrib_key)
46
- jsonapi_build_params.dig(:data, :attributes, attrib_key).present?
51
+ jsonapi_attribute(attrib_key).present?
47
52
  end
48
53
 
49
- def jsonapi_attribute_value(attrib_key)
50
- jsonapi_build_params.dig(:data, :attributes, attrib_key)
54
+ def jsonapi_relationship(attrib_key)
55
+ jsonapi_build_params.dig(:data, :relationships, attrib_key)
56
+ end
57
+
58
+ def jsonapi_relationship_data(attrib_key)
59
+ jsonapi_build_params.dig(:data, :relationships, attrib_key, :data)
60
+ end
61
+
62
+ def jsonapi_relationship_present?(attrib_key)
63
+ jsonapi_relationship(attrib_key).present?
64
+ end
65
+
66
+ def jsonapi_relationship_attribute(relationship, attribute)
67
+ if [:id, :type].include?(attribute.to_sym)
68
+ jsonapi_relationship_data(relationship)&.dig(attribute)
69
+ else
70
+ jsonapi_relationship_data(relationship)&.dig(:attributes, attribute)
71
+ end
51
72
  end
52
73
 
53
74
  def jsonapi_assign_params
54
- @jsonapi_assign_params ||= ParamsParser.parse_body_params(request,
55
- jsonapi_build_params,
56
- jsonapi_allowed_attributes,
57
- jsonapi_exclude_attributes,
58
- jsonapi_allowed_relationships,
59
- jsonapi_exclude_relationships)
75
+ return @jsonapi_assign_params if @jsonapi_assign_params.present? && !@invalidate_assign_params
76
+
77
+ @jsonapi_assign_params = ParamsParser.parse_body_params(request,
78
+ jsonapi_build_params,
79
+ jsonapi_allowed_attributes,
80
+ jsonapi_exclude_attributes,
81
+ jsonapi_allowed_relationships,
82
+ jsonapi_exclude_relationships)
83
+ @invalidate_assign_params = false
84
+ @jsonapi_assign_params
60
85
  end
61
86
 
62
87
  def jsonapi_exclude_attribute(attrib_key)
63
88
  @jsonapi_exclude_attributes ||= []
64
89
  @jsonapi_exclude_attributes << attrib_key.to_sym
90
+ @invalidate_assign_params = true
65
91
  jsonapi_build_params.dig(:data, :attributes, attrib_key)
66
92
  end
67
93
 
68
94
  def jsonapi_exclude_relationship(rel_key)
69
95
  @jsonapi_exclude_relationships ||= []
70
96
  @jsonapi_exclude_relationships << rel_key.to_sym
97
+ @invalidate_assign_params = true
71
98
  jsonapi_build_params.dig(:data, :relationships, rel_key)
72
99
  end
73
100
 
74
- # Should be overwritten in specific controllers
101
+ # Should be overwritten in specific controllers. If you need to manipulate params before they are parsed,
102
+ # that's the place to do it
75
103
  def jsonapi_build_params
76
104
  params
77
105
  end
@@ -108,19 +136,24 @@ module JsonApiable
108
136
  end
109
137
  end
110
138
 
139
+ def set_jsonapi_filter(filter_class)
140
+ @jsonapi_filter_class = filter_class
141
+ @jsonapi_filter_hash = FilterParser.parse_filters!(jsonapi_build_params, filter_class)
142
+ end
143
+
111
144
  def set_jsonapi_content_type
112
145
  response.headers['Content-Type'] = JSONAPI_CONTENT_TYPE
113
146
  end
114
147
 
115
148
  def parse_jsonapi_pagination
116
- @jsonapi_page = PaginationParser.parse_pagination!(query_params, jsonapi_default_page_size)
149
+ @jsonapi_page_hash = PaginationParser.parse_pagination!(query_params, jsonapi_default_page_size)
117
150
  end
118
151
 
119
152
  def parse_jsonapi_include
120
- @jsonapi_include = query_params[:include].presence&.gsub(/ /, '')&.split(',')&.map(&:to_sym).to_a
153
+ @jsonapi_include_array = query_params[:include].presence&.gsub(/ /, '')&.split(',')&.map(&:to_sym).to_a
121
154
  end
122
155
 
123
156
  def query_params
124
157
  request.query_parameters
125
158
  end
126
- end
159
+ end
@@ -1,6 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
4
  class PaginationParser
3
-
4
5
  def self.parse_pagination!(query_params, default_page_size)
5
6
  PaginationParser.new(query_params[:page], query_params[:no_pagination], default_page_size).parse!
6
7
  end
@@ -15,7 +16,7 @@ module JsonApiable
15
16
 
16
17
  def parse!
17
18
  if no_pagination
18
- jsonapi_page = nil
19
+ jsonapi_page_hash = nil
19
20
  elsif invalid_page_param?
20
21
  raise ArgumentError, 'page'
21
22
  elsif invalid_page_number?
@@ -23,14 +24,14 @@ module JsonApiable
23
24
  elsif invalid_page_size?
24
25
  raise ArgumentError, 'page[size]'
25
26
  else
26
- jsonapi_page = page_param.presence.to_h.with_indifferent_access
27
+ jsonapi_page_hash = page_param.presence.to_h.with_indifferent_access
27
28
  # convert values to integers
28
- jsonapi_page = jsonapi_page.merge(jsonapi_page) { |k,v| v.to_i } if jsonapi_page.present?
29
- jsonapi_page = { number: Configuration::DEFAULT_PAGE_NUMBER, size: default_page_size } if jsonapi_page.blank?
30
- jsonapi_page[:number] = Configuration::DEFAULT_PAGE_NUMBER if jsonapi_page[:number].blank?
31
- jsonapi_page[:size] = default_page_size if jsonapi_page[:size].blank?
29
+ jsonapi_page_hash = jsonapi_page_hash.merge(jsonapi_page_hash) { |_, v| v.to_i } if jsonapi_page_hash.present?
30
+ jsonapi_page_hash = { number: Configuration::DEFAULT_PAGE_NUMBER, size: default_page_size } if jsonapi_page_hash.blank?
31
+ jsonapi_page_hash[:number] = Configuration::DEFAULT_PAGE_NUMBER if jsonapi_page_hash[:number].blank?
32
+ jsonapi_page_hash[:size] = default_page_size if jsonapi_page_hash[:size].blank?
32
33
  end
33
- jsonapi_page
34
+ jsonapi_page_hash
34
35
  end
35
36
 
36
37
  private
@@ -56,4 +57,4 @@ module JsonApiable
56
57
  number > Configuration::MAX_PAGE_SIZE || number.zero? || number.negative?
57
58
  end
58
59
  end
59
- end
60
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
4
  class ParamsParser
3
5
  class DataParams
@@ -24,18 +26,7 @@ module JsonApiable
24
26
  end
25
27
 
26
28
  def self.validate_data_params!(params, attributes, relationships)
27
- permitted = DataParams.build(params, attributes, relationships)
28
- unpermitted = params.dig(:data)&.keys.to_a - permitted&.keys.to_a
29
- raise ArgumentError, "Unpermitted member: #{unpermitted.first}" if unpermitted.present?
30
-
31
-
32
- unpermitted_arguments = params.dig(:data, :attributes)&.keys.to_a - permitted.dig(:attributes)&.keys.to_a
33
- raise ArgumentError, "Unpermitted attribute: #{unpermitted_arguments.first}" if unpermitted_arguments.present?
34
-
35
- unpermitted_relationships = params.dig(:data, :relationships)&.keys.to_a - permitted.dig(:relationships)&.keys.to_a
36
- raise ArgumentError, "Unpermitted relationship: #{unpermitted_relationships.first}" if unpermitted_relationships.present?
37
-
38
- permitted
29
+ DataParams.build(params, attributes, relationships)
39
30
  end
40
31
 
41
32
  def self.build_attributes_hash(attributes, excluded_attributes)
@@ -50,7 +41,9 @@ module JsonApiable
50
41
  end
51
42
 
52
43
  def self.build_relationships_hash(relationships, excluded_relationships, request)
53
- attr_hash = {}; ids_array = []; ids_key = nil
44
+ attr_hash = {}
45
+ ids_array = []
46
+ ids_key = nil
54
47
 
55
48
  relationships&.each_pair do |key, data_hash|
56
49
  next if excluded_relationships&.include?(key.to_sym)
@@ -83,7 +76,7 @@ module JsonApiable
83
76
  end
84
77
 
85
78
  def self.hashify(allowed_relationships)
86
- allowed_relationships.map{|rel| { rel => {}}}
79
+ allowed_relationships.map { |rel| { rel => {} } }
87
80
  end
88
81
  end
89
- end
82
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
4
  module Renderers
3
5
  def respond_to_unsupported_media_type
@@ -49,4 +51,4 @@ module JsonApiable
49
51
  render json: { errors: json }, status: status
50
52
  end
51
53
  end
52
- end
54
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
- VERSION = "0.2.0"
4
+ VERSION = "0.5"
3
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_apiable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: '0.5'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Mike Polischuk
8
- autorequire:
8
+ autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-01-13 00:00:00.000000000 Z
11
+ date: 2021-05-03 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord
@@ -16,42 +16,42 @@ dependencies:
16
16
  requirements:
17
17
  - - ">="
18
18
  - !ruby/object:Gem::Version
19
- version: '4.2'
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: '4.2'
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: activesupport
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
31
  - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '4.2'
33
+ version: '0'
34
34
  type: :runtime
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '4.2'
40
+ version: '0'
41
41
  - !ruby/object:Gem::Dependency
42
- name: fast_jsonapi
42
+ name: jsonapi-serializer
43
43
  requirement: !ruby/object:Gem::Requirement
44
44
  requirements:
45
- - - "~>"
45
+ - - ">="
46
46
  - !ruby/object:Gem::Version
47
- version: '1.5'
47
+ version: '0'
48
48
  type: :runtime
49
49
  prerelease: false
50
50
  version_requirements: !ruby/object:Gem::Requirement
51
51
  requirements:
52
- - - "~>"
52
+ - - ">="
53
53
  - !ruby/object:Gem::Version
54
- version: '1.5'
54
+ version: '0'
55
55
  - !ruby/object:Gem::Dependency
56
56
  name: bundler
57
57
  requirement: !ruby/object:Gem::Requirement
@@ -156,14 +156,14 @@ dependencies:
156
156
  requirements:
157
157
  - - "~>"
158
158
  - !ruby/object:Gem::Version
159
- version: '10.0'
159
+ version: '13.0'
160
160
  type: :development
161
161
  prerelease: false
162
162
  version_requirements: !ruby/object:Gem::Requirement
163
163
  requirements:
164
164
  - - "~>"
165
165
  - !ruby/object:Gem::Version
166
- version: '10.0'
166
+ version: '13.0'
167
167
  - !ruby/object:Gem::Dependency
168
168
  name: rspec-rails
169
169
  requirement: !ruby/object:Gem::Requirement
@@ -192,7 +192,7 @@ dependencies:
192
192
  - - ">="
193
193
  - !ruby/object:Gem::Version
194
194
  version: '0'
195
- description:
195
+ description:
196
196
  email:
197
197
  - mike.polis@gmail.com
198
198
  executables: []
@@ -201,6 +201,7 @@ extra_rdoc_files: []
201
201
  files:
202
202
  - ".gitignore"
203
203
  - ".rspec"
204
+ - ".rubocop.yml"
204
205
  - ".travis.yml"
205
206
  - Gemfile
206
207
  - Gemfile.lock
@@ -211,9 +212,12 @@ files:
211
212
  - bin/setup
212
213
  - json_apiable.gemspec
213
214
  - lib/json_apiable.rb
215
+ - lib/json_apiable/base_filter.rb
214
216
  - lib/json_apiable/configuration.rb
215
217
  - lib/json_apiable/core_extensions.rb
216
218
  - lib/json_apiable/errors.rb
219
+ - lib/json_apiable/filter_matchers.rb
220
+ - lib/json_apiable/filter_parser.rb
217
221
  - lib/json_apiable/json_apiable.rb
218
222
  - lib/json_apiable/pagination_parser.rb
219
223
  - lib/json_apiable/params_parser.rb
@@ -224,7 +228,7 @@ licenses:
224
228
  - MIT
225
229
  metadata:
226
230
  homepage_uri: http://github.com/mikemarsian/json_apiable
227
- post_install_message:
231
+ post_install_message:
228
232
  rdoc_options: []
229
233
  require_paths:
230
234
  - lib
@@ -239,8 +243,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
239
243
  - !ruby/object:Gem::Version
240
244
  version: '0'
241
245
  requirements: []
242
- rubygems_version: 3.0.6
243
- signing_key:
246
+ rubygems_version: 3.1.4
247
+ signing_key:
244
248
  specification_version: 4
245
249
  summary: Include JsonApiable module in your API::BaseController to receive a collection
246
250
  of useful methods, such as arguments and relationships parser, filters, etc.