json_apiable 0.3 → 0.5.2

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: 1d67de4927cb04df83deea484a0890bdfdc36cbf48d6bda986000ffabcda8f3b
4
- data.tar.gz: a90a940bc16d451c698355f1e18c74a5e183135ed3315f7aa272ad9a37e345f5
3
+ metadata.gz: 8badac374f5beeb1a450a13c9e15a9b51ecf3f7999a1584037df80234746365e
4
+ data.tar.gz: 9731a6810c7bde962dfd7ff5874f18366b6bb378a7a0e4358200249ef12f569b
5
5
  SHA512:
6
- metadata.gz: ee5e99fac5548996d4222e3b281ca8d3d03c78e95b57414c08b8f6f624497140c1eff43d0f5e5d54a3c2c2fec9501e92b09d37deb2e5c688c132161ca88f031c
7
- data.tar.gz: 8d36927df5f8684188cc4e6f4ef263640429ddff9a84f9824860fe1ff1519c94fdb1b90642291681d5612478e9913ca818539a1f2c8d642657dba2d24d0f6a48
6
+ metadata.gz: e0a1e8c16896ba6bb97165654a6fa31d2565eb2ae03a9a1fb464ee44f62cbf73b9556fc1f68366b2b3a54665bcd2ad66e84c7947200efe345a3de185f3259a60
7
+ data.tar.gz: 31e1bc0ce9505acc7234cc6dc0f1866ee1f7aa99411acedac3bff8db8d74360fa256b6e3fa11c969f2c8b6bb68f352604b5419f6b4c980fbe727975de63d48d4
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.3)
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,7 +99,7 @@ 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
@@ -111,7 +113,7 @@ class API::PostsController < API::BaseController
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
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
@@ -18,6 +20,7 @@ module JsonApiable
18
20
 
19
21
  rescue_from ArgumentError, with: :respond_to_bad_argument
20
22
  rescue_from ActionController::UnpermittedParameters, with: :respond_to_bad_argument
23
+ rescue_from JSONAPI::Serializer::UnsupportedIncludeError, with: :respond_to_exception_raised
21
24
  rescue_from MalformedRequestError, with: :respond_to_malformed_request
22
25
  rescue_from UnprocessableEntityError, with: :respond_to_unprocessable_entity
23
26
  rescue_from UnauthorizedError, with: :respond_to_unauthorized
@@ -25,7 +28,6 @@ module JsonApiable
25
28
  rescue_from JsonApiable.configuration.not_found_exception_class, with: :respond_to_not_found
26
29
  end
27
30
 
28
-
29
31
  class << self
30
32
  attr_writer :configuration
31
33
  end
@@ -63,28 +65,37 @@ module JsonApiable
63
65
  end
64
66
 
65
67
  def jsonapi_relationship_attribute(relationship, attribute)
66
- [:id, :type].include?(attribute.to_sym) ? jsonapi_relationship_data(relationship)&.dig(attribute) :
67
- jsonapi_relationship_data(relationship)&.dig(:attributes, attribute)
68
+ if [:id, :type].include?(attribute.to_sym)
69
+ jsonapi_relationship_data(relationship)&.dig(attribute)
70
+ else
71
+ jsonapi_relationship_data(relationship)&.dig(:attributes, attribute)
72
+ end
68
73
  end
69
74
 
70
75
  def jsonapi_assign_params
71
- @jsonapi_assign_params ||= ParamsParser.parse_body_params(request,
72
- jsonapi_build_params,
73
- jsonapi_allowed_attributes,
74
- jsonapi_exclude_attributes,
75
- jsonapi_allowed_relationships,
76
- jsonapi_exclude_relationships)
76
+ return @jsonapi_assign_params if @jsonapi_assign_params.present? && !@invalidate_assign_params
77
+
78
+ @jsonapi_assign_params = ParamsParser.parse_body_params(request,
79
+ jsonapi_build_params,
80
+ jsonapi_allowed_attributes,
81
+ jsonapi_exclude_attributes,
82
+ jsonapi_allowed_relationships,
83
+ jsonapi_exclude_relationships)
84
+ @invalidate_assign_params = false
85
+ @jsonapi_assign_params
77
86
  end
78
87
 
79
88
  def jsonapi_exclude_attribute(attrib_key)
80
89
  @jsonapi_exclude_attributes ||= []
81
90
  @jsonapi_exclude_attributes << attrib_key.to_sym
91
+ @invalidate_assign_params = true
82
92
  jsonapi_build_params.dig(:data, :attributes, attrib_key)
83
93
  end
84
94
 
85
95
  def jsonapi_exclude_relationship(rel_key)
86
96
  @jsonapi_exclude_relationships ||= []
87
97
  @jsonapi_exclude_relationships << rel_key.to_sym
98
+ @invalidate_assign_params = true
88
99
  jsonapi_build_params.dig(:data, :relationships, rel_key)
89
100
  end
90
101
 
@@ -126,19 +137,24 @@ module JsonApiable
126
137
  end
127
138
  end
128
139
 
140
+ def set_jsonapi_filter(filter_class)
141
+ @jsonapi_filter_class = filter_class
142
+ @jsonapi_filter_hash = FilterParser.parse_filters!(jsonapi_build_params, filter_class)
143
+ end
144
+
129
145
  def set_jsonapi_content_type
130
146
  response.headers['Content-Type'] = JSONAPI_CONTENT_TYPE
131
147
  end
132
148
 
133
149
  def parse_jsonapi_pagination
134
- @jsonapi_page = PaginationParser.parse_pagination!(query_params, jsonapi_default_page_size)
150
+ @jsonapi_page_hash = PaginationParser.parse_pagination!(query_params, jsonapi_default_page_size)
135
151
  end
136
152
 
137
153
  def parse_jsonapi_include
138
- @jsonapi_include = query_params[:include].presence&.gsub(/ /, '')&.split(',')&.map(&:to_sym).to_a
154
+ @jsonapi_include_array = query_params[:include].presence&.gsub(/ /, '')&.split(',')&.map(&:to_sym).to_a
139
155
  end
140
156
 
141
157
  def query_params
142
158
  request.query_parameters
143
159
  end
144
- end
160
+ 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
@@ -39,7 +41,9 @@ module JsonApiable
39
41
  end
40
42
 
41
43
  def self.build_relationships_hash(relationships, excluded_relationships, request)
42
- attr_hash = {}; ids_array = []; ids_key = nil
44
+ attr_hash = {}
45
+ ids_array = []
46
+ ids_key = nil
43
47
 
44
48
  relationships&.each_pair do |key, data_hash|
45
49
  next if excluded_relationships&.include?(key.to_sym)
@@ -72,7 +76,7 @@ module JsonApiable
72
76
  end
73
77
 
74
78
  def self.hashify(allowed_relationships)
75
- allowed_relationships.map{|rel| { rel => {}}}
79
+ allowed_relationships.map { |rel| { rel => {} } }
76
80
  end
77
81
  end
78
- 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
@@ -30,6 +32,11 @@ module JsonApiable
30
32
  json_render_errors json: errors, status: :bad_request
31
33
  end
32
34
 
35
+ def respond_to_exception_raised(err_msg)
36
+ errors = [{ title: 'Invalid Argument', detail: err_msg.message }]
37
+ json_render_errors json: errors, status: :bad_request
38
+ end
39
+
33
40
  def respond_to_malformed_request(err_msg = nil)
34
41
  errors = [{ title: 'Malformed Request', detail: err_msg.to_s }]
35
42
  json_render_errors json: errors, status: :bad_request
@@ -49,4 +56,4 @@ module JsonApiable
49
56
  render json: { errors: json }, status: status
50
57
  end
51
58
  end
52
- end
59
+ end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module JsonApiable
2
- VERSION = "0.3"
4
+ VERSION = "0.5.2"
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.3'
4
+ version: 0.5.2
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-19 00:00:00.000000000 Z
11
+ date: 2021-05-06 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.