json_apiable 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: b0527cb82ea3c0689d6086d1728291e77bff1cd94da969953b8965e729eb66b6
4
+ data.tar.gz: e98ff51f719ca22f14574c9acdb4a8b1175e13d513b8f93f2595d4d83cdffa89
5
+ SHA512:
6
+ metadata.gz: c424b59b59efa76d8f04c248345ce5a42b9c2b2f53bf539a73f0eea4e4b4551186f32c8923fc097182c54e123582e9a4bdfdf7a715e5417de724bbe485715715
7
+ data.tar.gz: 73af3e75acfb6a3e8a69fafdb338882cbcd2e6bcc38d5b9e65600ee5824b7af98ae650e38ec8520fe9b1f38a2591d34652081a18e3d1df1d977c072c5ecfdc48
data/.gitignore ADDED
@@ -0,0 +1,15 @@
1
+ /.bundle/
2
+ /.idea/
3
+ /.yardoc
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ /log/
11
+ *.sqlite3
12
+
13
+ # rspec failure tracking
14
+ .rspec_status
15
+ /spec/support/rails_app/db/*.sqlite3
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.5
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in json_apiable.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,193 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ json_apiable (0.1.0)
5
+ activerecord (>= 4.2)
6
+ activesupport (>= 4.2)
7
+ fast_jsonapi (~> 1.5)
8
+
9
+ GEM
10
+ remote: https://rubygems.org/
11
+ specs:
12
+ actioncable (6.0.2.1)
13
+ actionpack (= 6.0.2.1)
14
+ nio4r (~> 2.0)
15
+ 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)
22
+ 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)
27
+ mail (~> 2.5, >= 2.5.4)
28
+ 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)
33
+ rack-test (>= 0.6.3)
34
+ rails-dom-testing (~> 2.0)
35
+ 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)
41
+ nokogiri (>= 1.8.5)
42
+ actionview (6.0.2.1)
43
+ activesupport (= 6.0.2.1)
44
+ builder (~> 3.1)
45
+ erubi (~> 1.4)
46
+ rails-dom-testing (~> 2.0)
47
+ rails-html-sanitizer (~> 1.1, >= 1.2.0)
48
+ activejob (6.0.2.1)
49
+ activesupport (= 6.0.2.1)
50
+ 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)
62
+ 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
+ builder (3.2.4)
68
+ byebug (11.0.1)
69
+ coderay (1.1.2)
70
+ concurrent-ruby (1.1.5)
71
+ crass (1.0.5)
72
+ 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)
83
+ globalid (0.4.2)
84
+ activesupport (>= 4.2.0)
85
+ i18n (1.7.0)
86
+ concurrent-ruby (~> 1.0)
87
+ loofah (2.4.0)
88
+ crass (~> 1.0.2)
89
+ nokogiri (>= 1.5.9)
90
+ mail (2.7.1)
91
+ mini_mime (>= 0.1.1)
92
+ marcel (0.3.3)
93
+ mimemagic (~> 0.3.2)
94
+ 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)
102
+ pry (0.12.2)
103
+ coderay (~> 1.1.0)
104
+ method_source (~> 0.9.0)
105
+ pry-byebug (3.7.0)
106
+ byebug (~> 11.0)
107
+ pry (~> 0.10)
108
+ rack (2.0.8)
109
+ rack-test (1.1.0)
110
+ 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)
125
+ sprockets-rails (>= 2.0.0)
126
+ rails-controller-testing (1.0.4)
127
+ actionpack (>= 5.0.1.x)
128
+ actionview (>= 5.0.1.x)
129
+ activesupport (>= 5.0.1.x)
130
+ rails-dom-testing (2.0.3)
131
+ activesupport (>= 4.2.0)
132
+ nokogiri (>= 1.6)
133
+ rails-html-sanitizer (1.3.0)
134
+ loofah (~> 2.3)
135
+ railties (6.0.2.1)
136
+ actionpack (= 6.0.2.1)
137
+ activesupport (= 6.0.2.1)
138
+ method_source
139
+ 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
+ rspec-expectations (3.9.0)
145
+ diff-lcs (>= 1.2.0, < 2.0)
146
+ rspec-support (~> 3.9.0)
147
+ rspec-mocks (3.9.0)
148
+ diff-lcs (>= 1.2.0, < 2.0)
149
+ rspec-support (~> 3.9.0)
150
+ rspec-rails (3.9.0)
151
+ actionpack (>= 3.0)
152
+ activesupport (>= 3.0)
153
+ railties (>= 3.0)
154
+ rspec-core (~> 3.9.0)
155
+ rspec-expectations (~> 3.9.0)
156
+ rspec-mocks (~> 3.9.0)
157
+ rspec-support (~> 3.9.0)
158
+ rspec-support (3.9.0)
159
+ sprockets (4.0.0)
160
+ concurrent-ruby (~> 1.0)
161
+ rack (> 1, < 3)
162
+ sprockets-rails (3.2.1)
163
+ actionpack (>= 4.0)
164
+ activesupport (>= 4.0)
165
+ sprockets (>= 3.0.0)
166
+ 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)
172
+ websocket-extensions (>= 0.1.0)
173
+ websocket-extensions (0.1.4)
174
+ zeitwerk (2.2.2)
175
+
176
+ PLATFORMS
177
+ ruby
178
+
179
+ DEPENDENCIES
180
+ bundler (~> 2.0)
181
+ factory_bot_rails
182
+ faker
183
+ json_apiable!
184
+ pry
185
+ pry-byebug
186
+ rails
187
+ rails-controller-testing
188
+ rake (~> 10.0)
189
+ rspec-rails
190
+ sqlite3
191
+
192
+ BUNDLED WITH
193
+ 2.1.1
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 mike
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,39 @@
1
+ # JsonApiable
2
+
3
+ Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/json_apiable`. To experiment with that code, run `bin/console` for an interactive prompt.
4
+
5
+ TODO: Delete this and the text above, and describe your gem
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem 'json_apiable'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install json_apiable
22
+
23
+ ## Usage
24
+
25
+ TODO: Write usage instructions here
26
+
27
+ ## Development
28
+
29
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
30
+
31
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
32
+
33
+ ## Contributing
34
+
35
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/json_apiable.
36
+
37
+ ## License
38
+
39
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "json_apiable"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ require "pry"
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,41 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "json_apiable/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "json_apiable"
7
+ spec.version = JsonApiable::VERSION
8
+ spec.authors = ["Mike Polischuk"]
9
+ spec.email = ["mike.polis@gmail.com"]
10
+
11
+ spec.summary = %q{Include JsonApiable module in your API::BaseController to receive a collection of useful
12
+ methods, such as arguments and relationships parser, filters, etc.}
13
+ spec.homepage = "http://github.com/mikemarsian/json_apiable"
14
+ spec.license = "MIT"
15
+
16
+ spec.metadata["homepage_uri"] = spec.homepage
17
+
18
+ # Specify which files should be added to the gem when it is released.
19
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
20
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
21
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
22
+ end
23
+ spec.bindir = "exe"
24
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
25
+ spec.require_paths = ["lib"]
26
+
27
+ spec.add_dependency 'activerecord', '>= 4.2'
28
+ spec.add_dependency 'activesupport', '>= 4.2'
29
+ spec.add_dependency 'fast_jsonapi', '~> 1.5'
30
+
31
+ spec.add_development_dependency 'bundler', '~> 2.0'
32
+ spec.add_development_dependency 'factory_bot_rails'
33
+ spec.add_development_dependency 'faker'
34
+ spec.add_development_dependency 'pry'
35
+ spec.add_development_dependency 'pry-byebug'
36
+ spec.add_development_dependency 'rails'
37
+ spec.add_development_dependency 'rails-controller-testing'
38
+ spec.add_development_dependency 'rake', '~> 10.0'
39
+ spec.add_development_dependency 'rspec-rails'
40
+ spec.add_development_dependency 'sqlite3'
41
+ end
@@ -0,0 +1,32 @@
1
+ module JsonApiable
2
+ class Configuration
3
+ attr_accessor :valid_query_params, :supported_media_type_proc, :not_found_exception_class, :page_size
4
+
5
+ DEFAULT_PAGE_NUMBER = 1
6
+ DEFAULT_PAGE_SIZE = 25
7
+ # 2^32 / 2 * 10: greater than which raises a java.lang.ArrayIndexOutOfBoundsException exception in Solr
8
+ MAX_PAGE_SIZE = 214_748_364
9
+
10
+ def initialize
11
+ @valid_query_params = %w[id access_token filter include page]
12
+ @supported_media_type_proc = nil
13
+ @not_found_exception_class = ActiveRecord::RecordNotFound
14
+ @page_size = DEFAULT_PAGE_SIZE
15
+ end
16
+
17
+ def valid_query_params=(value)
18
+ raise JsonApiable::ConfigurationError, 'Should be an array containing strings' unless value.is_a?(Array)
19
+ @valid_query_params = value
20
+ end
21
+
22
+ def supported_media_type_proc=(prok)
23
+ raise JsonApiable::ConfigurationError, 'Should be a proc' unless prok.is_a?(Proc)
24
+ @supported_media_type_proc = prok
25
+ end
26
+
27
+ def not_found_exception_class=(klass)
28
+ raise JsonApiable::ConfigurationError, 'Should be a class' unless klass.is_a?(Class)
29
+ @not_found_exception_class = klass
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,7 @@
1
+ module CoreExtensions
2
+ module String
3
+ def integer?
4
+ to_i.to_s == self
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,10 @@
1
+ module JsonApiable
2
+ module Errors
3
+ class ApiError < StandardError; end
4
+ class MalformedRequestError < ApiError; end
5
+ class UnprocessableEntityError < ApiError; end
6
+ class UnauthorizedError < ApiError; end
7
+ class ForbiddenError < ApiError; end
8
+ class ConfigurationError < ApiError; end
9
+ end
10
+ end
@@ -0,0 +1,122 @@
1
+ module JsonApiable
2
+ extend ActiveSupport::Concern
3
+ include Errors
4
+ include Renderers
5
+
6
+ JSONAPI_CONTENT_TYPE = 'application/vnd.api+json'
7
+
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
+
11
+ included do
12
+ before_action :ensure_content_type
13
+ before_action :ensure_valid_query_params
14
+ before_action :parse_pagination
15
+ before_action :parse_include
16
+
17
+ after_action :set_content_type
18
+
19
+ rescue_from ArgumentError, with: :respond_to_bad_argument
20
+ rescue_from ActionController::UnpermittedParameters, with: :respond_to_bad_argument
21
+ rescue_from MalformedRequestError, with: :respond_to_malformed_request
22
+ rescue_from UnprocessableEntityError, with: :respond_to_unprocessable_entity
23
+ rescue_from UnauthorizedError, with: :respond_to_unauthorized
24
+ rescue_from ForbiddenError, with: :respond_to_forbidden
25
+ rescue_from JsonApiable.configuration.not_found_exception_class, with: :respond_to_not_found
26
+ end
27
+
28
+
29
+ class << self
30
+ attr_writer :configuration
31
+ end
32
+
33
+ def self.configuration
34
+ @configuration ||= Configuration.new
35
+ end
36
+
37
+ def self.reset
38
+ @configuration = Configuration.new
39
+ end
40
+
41
+ def self.configure
42
+ yield(configuration)
43
+ end
44
+
45
+ def jsonapi_attribute_present?(attrib_key)
46
+ jsonapi_build_params.dig(:data, :attributes, attrib_key).present?
47
+ end
48
+
49
+ def jsonapi_assign_params
50
+ @jsonapi_assign_params ||= ParamsParser.parse_body_params(request,
51
+ jsonapi_build_params,
52
+ jsonapi_allowed_attributes,
53
+ jsonapi_exclude_attributes,
54
+ jsonapi_allowed_relationships,
55
+ jsonapi_exclude_relationships)
56
+ end
57
+
58
+ def jsonapi_exclude_attribute(attrib_key)
59
+ @jsonapi_exclude_attributes ||= []
60
+ @jsonapi_exclude_attributes << attrib_key.to_sym
61
+ jsonapi_build_params.dig(:data, :attributes, attrib_key)
62
+ end
63
+
64
+ def jsonapi_exclude_relationship(rel_key)
65
+ @jsonapi_exclude_relationships ||= []
66
+ @jsonapi_exclude_relationships << rel_key.to_sym
67
+ jsonapi_build_params.dig(:data, :relationships, rel_key)
68
+ end
69
+
70
+ # Should be overwritten in specific controllers
71
+ def jsonapi_build_params
72
+ params
73
+ end
74
+
75
+ # Should be overwritten in specific controllers
76
+ def jsonapi_default_page_size
77
+ JsonApiable.configuration.page_size
78
+ end
79
+
80
+ # Should be overwritten in specific controllers
81
+ def jsonapi_allowed_attributes
82
+ %i[]
83
+ end
84
+
85
+ # Should be overwritten in specific controllers
86
+ def jsonapi_allowed_relationships
87
+ %i[]
88
+ end
89
+
90
+ def ensure_content_type
91
+ respond_to_unsupported_media_type unless supported_media_type?
92
+ end
93
+
94
+ def ensure_valid_query_params
95
+ invalid_params = request.query_parameters.keys.reject { |k| JsonApiable.configuration.valid_query_params.include?(k) }
96
+ respond_to_bad_argument(invalid_params.first) if invalid_params.present?
97
+ end
98
+
99
+ def supported_media_type?
100
+ if JsonApiable.configuration.supported_media_type_proc.present?
101
+ JsonApiable.configuration.supported_media_type_proc.call(request)
102
+ else
103
+ request.content_type == JSONAPI_CONTENT_TYPE
104
+ end
105
+ end
106
+
107
+ def set_content_type
108
+ response.headers['Content-Type'] = JSONAPI_CONTENT_TYPE
109
+ end
110
+
111
+ def parse_pagination
112
+ @jsonapi_page = PaginationParser.parse_pagination!(query_params, jsonapi_default_page_size)
113
+ end
114
+
115
+ def parse_include
116
+ @jsonapi_include = query_params[:include].presence&.gsub(/ /, '')&.split(',')&.map(&:to_sym).to_a
117
+ end
118
+
119
+ def query_params
120
+ request.query_parameters
121
+ end
122
+ end
@@ -0,0 +1,59 @@
1
+ module JsonApiable
2
+ class PaginationParser
3
+
4
+ def self.parse_pagination!(query_params, default_page_size)
5
+ PaginationParser.new(query_params[:page], query_params[:no_pagination], default_page_size).parse!
6
+ end
7
+
8
+ attr_reader :page_param, :no_pagination, :default_page_size
9
+
10
+ def initialize(page_arg, no_pagination_arg, default_page_size)
11
+ @page_param = page_arg
12
+ @no_pagination = no_pagination_arg
13
+ @default_page_size = default_page_size
14
+ end
15
+
16
+ def parse!
17
+ if no_pagination
18
+ jsonapi_page = nil
19
+ elsif invalid_page_param?
20
+ raise ArgumentError, 'page'
21
+ elsif invalid_page_number?
22
+ raise ArgumentError, 'page[number]'
23
+ elsif invalid_page_size?
24
+ raise ArgumentError, 'page[size]'
25
+ else
26
+ jsonapi_page = page_param.presence.to_h.with_indifferent_access
27
+ # 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?
32
+ end
33
+ jsonapi_page
34
+ end
35
+
36
+ private
37
+
38
+ def invalid_page_param?
39
+ page_param.present? && !page_param.is_a?(HashWithIndifferentAccess)
40
+ end
41
+
42
+ def invalid_page_number?
43
+ page_num_param = page_param&.dig(:number)
44
+ page_num_param.present? && invalid_number?(page_num_param)
45
+ end
46
+
47
+ def invalid_page_size?
48
+ page_size_param = page_param&.dig(:size)
49
+ page_size_param.present? && invalid_number?(page_size_param)
50
+ end
51
+
52
+ def invalid_number?(number_param)
53
+ return true unless number_param.integer?
54
+
55
+ number = number_param.to_i
56
+ number > Configuration::MAX_PAGE_SIZE || number.zero? || number.negative?
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,89 @@
1
+ module JsonApiable
2
+ class ParamsParser
3
+ class DataParams
4
+ def self.build(params, attributes, relationships)
5
+ params.require(:data).permit(:id, :type,
6
+ attributes: attributes,
7
+ relationships: relationships)
8
+ end
9
+ end
10
+
11
+ # Convert JsonAPI request body into Rails params hash
12
+ def self.parse_body_params(request, params, allowed_attributes, excluded_attributes,
13
+ allowed_relationships, excluded_relationships)
14
+ permitted_params = validate_data_params!(params,
15
+ allowed_attributes,
16
+ hashify(allowed_relationships))
17
+ attributes_hash = build_attributes_hash(permitted_params.dig(:attributes), excluded_attributes)
18
+ relationships_hash = build_relationships_hash(permitted_params.dig(:relationships), excluded_relationships, request)
19
+ attributes_hash.merge(relationships_hash)
20
+ rescue ArgumentError, ActionController::UnpermittedParameters
21
+ raise
22
+ rescue StandardError => e
23
+ raise Errors::MalformedRequestError, e.message
24
+ end
25
+
26
+ 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
39
+ end
40
+
41
+ def self.build_attributes_hash(attributes, excluded_attributes)
42
+ attrs_hash = {}
43
+ attributes&.each do |key, value|
44
+ next if excluded_attributes&.include?(key.to_sym)
45
+
46
+ new_key = value.is_a?(ActionController::Parameters) ? "#{key}_attributes" : key
47
+ attrs_hash[new_key] = value.is_a?(ActionController::Parameters) ? value.to_h : value
48
+ end
49
+ attrs_hash
50
+ end
51
+
52
+ def self.build_relationships_hash(relationships, excluded_relationships, request)
53
+ attr_hash = {}; ids_array = []; ids_key = nil
54
+
55
+ relationships&.each_pair do |key, data_hash|
56
+ next if excluded_relationships&.include?(key.to_sym)
57
+
58
+ if ActiveSupport::Inflector.pluralize(key) == key
59
+ new_key = "#{key}_attributes"
60
+ new_value = build_relationship_attribute_hash(data_hash)
61
+ ids_key = "#{ActiveSupport::Inflector.singularize(key)}_ids"
62
+ ids_array = data_hash['data']&.map { |h| h['id'] }
63
+ else
64
+ new_key = "#{key}_id"
65
+ new_value = data_hash['data']['id']
66
+ end
67
+ attr_hash[new_key] = new_value
68
+ # ids array is needed when creating a new AR object which accepts_nested_attributes for existing AR object(s)
69
+ # as in the case of a new Message which accepts existing attachments
70
+ # https://stackoverflow.com/a/25943832/1983833
71
+ attr_hash[ids_key] = ids_array if ids_array.present? && (request.post? || request.patch?)
72
+ end
73
+ attr_hash
74
+ end
75
+
76
+ def self.build_relationship_attribute_hash(data_hash)
77
+ attr_hash = {}
78
+ data_hash['data'].each_with_index do |arr_item, i|
79
+ item_hash = { 'id' => arr_item['id'], '_destroy' => 'false' }
80
+ attr_hash[i.to_s] = item_hash
81
+ end
82
+ attr_hash
83
+ end
84
+
85
+ def self.hashify(allowed_relationships)
86
+ allowed_relationships.map{|rel| { rel => {}}}
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,52 @@
1
+ module JsonApiable
2
+ module Renderers
3
+ def respond_to_unsupported_media_type
4
+ errors = [{ title: 'Unsupported Media Type', detail: 'application/vnd.api+json is expected' }]
5
+ json_render_errors json: errors, status: :unsupported_media_type
6
+ end
7
+
8
+ def respond_to_unprocessable_entity(err_msg = nil)
9
+ errors = [{ title: 'Unprocessable', detail: err_msg.to_s }]
10
+ json_render_errors json: errors, status: :unprocessable_entity
11
+ end
12
+
13
+ def respond_to_forbidden(err_msg = nil)
14
+ errors = [{ title: 'Forbidden', detail: err_msg.to_s || 'You are not authorized to perform this action' }]
15
+ json_render_errors json: errors, status: :forbidden
16
+ end
17
+
18
+ def respond_to_unauthorized(err_msg = nil)
19
+ errors = [{ title: 'Unauthorized', detail: err_msg.to_s || 'You have to be authenticated to perform this action' }]
20
+ json_render_errors json: errors, status: :unauthorized
21
+ end
22
+
23
+ def respond_to_not_found(err_msg = nil)
24
+ errors = [{ title: 'Not Found', detail: err_msg.to_s || 'Resource not found on the server' }]
25
+ json_render_errors json: errors, status: :not_found
26
+ end
27
+
28
+ def respond_to_bad_argument(err_msg)
29
+ errors = [{ title: 'Invalid Argument', detail: err_msg.to_s }]
30
+ json_render_errors json: errors, status: :bad_request
31
+ end
32
+
33
+ def respond_to_malformed_request(err_msg = nil)
34
+ errors = [{ title: 'Malformed Request', detail: err_msg.to_s }]
35
+ json_render_errors json: errors, status: :bad_request
36
+ end
37
+
38
+ def respond_to_capability_error
39
+ errors = [{ title: 'Capability Error', detail: "Your plan doesn't allow this action" }]
40
+ json_render_errors json: errors, status: :forbidden
41
+ end
42
+
43
+ def json_render_errors(json: nil, status: nil)
44
+ err_json = json.first
45
+ if err_json.present? && err_json[:status].blank?
46
+ status_code = status.is_a?(Symbol) ? Rack::Utils::SYMBOL_TO_STATUS_CODE[status] : status
47
+ err_json[:status] = status_code.to_s
48
+ end
49
+ render json: { errors: json }, status: status
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,3 @@
1
+ module JsonApiable
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,12 @@
1
+ require 'active_support/all'
2
+ require "json_apiable/version"
3
+ require "json_apiable/core_extensions"
4
+ require 'json_apiable/configuration'
5
+ require 'json_apiable/renderers'
6
+ require 'json_apiable/errors'
7
+ require 'json_apiable/params_parser'
8
+ require 'json_apiable/pagination_parser'
9
+ require 'json_apiable/json_apiable'
10
+
11
+ String.include CoreExtensions::String
12
+ Mime::Type.register JsonApiable::JSONAPI_CONTENT_TYPE, :json_api
metadata ADDED
@@ -0,0 +1,247 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: json_apiable
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Mike Polischuk
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-01-06 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activerecord
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '4.2'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '4.2'
27
+ - !ruby/object:Gem::Dependency
28
+ name: activesupport
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '4.2'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '4.2'
41
+ - !ruby/object:Gem::Dependency
42
+ name: fast_jsonapi
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.5'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.5'
55
+ - !ruby/object:Gem::Dependency
56
+ name: bundler
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '2.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '2.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: factory_bot_rails
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: faker
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: pry
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: pry-byebug
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: rails
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: rails-controller-testing
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: rake
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '10.0'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '10.0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec-rails
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ - !ruby/object:Gem::Dependency
182
+ name: sqlite3
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '0'
188
+ type: :development
189
+ prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '0'
195
+ description:
196
+ email:
197
+ - mike.polis@gmail.com
198
+ executables: []
199
+ extensions: []
200
+ extra_rdoc_files: []
201
+ files:
202
+ - ".gitignore"
203
+ - ".rspec"
204
+ - ".travis.yml"
205
+ - Gemfile
206
+ - Gemfile.lock
207
+ - LICENSE.txt
208
+ - README.md
209
+ - Rakefile
210
+ - bin/console
211
+ - bin/setup
212
+ - json_apiable.gemspec
213
+ - lib/json_apiable.rb
214
+ - lib/json_apiable/configuration.rb
215
+ - lib/json_apiable/core_extensions.rb
216
+ - lib/json_apiable/errors.rb
217
+ - lib/json_apiable/json_apiable.rb
218
+ - lib/json_apiable/pagination_parser.rb
219
+ - lib/json_apiable/params_parser.rb
220
+ - lib/json_apiable/renderers.rb
221
+ - lib/json_apiable/version.rb
222
+ homepage: http://github.com/mikemarsian/json_apiable
223
+ licenses:
224
+ - MIT
225
+ metadata:
226
+ homepage_uri: http://github.com/mikemarsian/json_apiable
227
+ post_install_message:
228
+ rdoc_options: []
229
+ require_paths:
230
+ - lib
231
+ required_ruby_version: !ruby/object:Gem::Requirement
232
+ requirements:
233
+ - - ">="
234
+ - !ruby/object:Gem::Version
235
+ version: '0'
236
+ required_rubygems_version: !ruby/object:Gem::Requirement
237
+ requirements:
238
+ - - ">="
239
+ - !ruby/object:Gem::Version
240
+ version: '0'
241
+ requirements: []
242
+ rubygems_version: 3.0.6
243
+ signing_key:
244
+ specification_version: 4
245
+ summary: Include JsonApiable module in your API::BaseController to receive a collection
246
+ of useful methods, such as arguments and relationships parser, filters, etc.
247
+ test_files: []