simple_jsonapi_rails 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +10 -0
- data/.rubocop.yml +132 -0
- data/CHANGELOG.md +2 -0
- data/Gemfile +5 -0
- data/Jenkinsfile +92 -0
- data/LICENSE.txt +22 -0
- data/README.md +245 -0
- data/Rakefile +10 -0
- data/lib/simple_jsonapi/errors/active_model_error.rb +21 -0
- data/lib/simple_jsonapi/errors/active_model_error_serializer.rb +15 -0
- data/lib/simple_jsonapi/errors/active_record/record_not_found_serializer.rb +15 -0
- data/lib/simple_jsonapi/rails/action_controller/jsonapi_helper.rb +122 -0
- data/lib/simple_jsonapi/rails/action_controller/request_validator.rb +40 -0
- data/lib/simple_jsonapi/rails/action_controller.rb +44 -0
- data/lib/simple_jsonapi/rails/extensions/routing.rb +57 -0
- data/lib/simple_jsonapi/rails/extensions.rb +13 -0
- data/lib/simple_jsonapi/rails/railtie.rb +34 -0
- data/lib/simple_jsonapi/rails/test_helpers.rb +20 -0
- data/lib/simple_jsonapi/rails/version.rb +5 -0
- data/lib/simple_jsonapi/rails.rb +13 -0
- data/lib/simple_jsonapi_rails.rb +4 -0
- data/simple_jsonapi_rails.gemspec +33 -0
- data/test/action_controller_test.rb +299 -0
- data/test/dummy/.gitignore +23 -0
- data/test/dummy/Rakefile +6 -0
- data/test/dummy/app/controllers/api_controller.rb +3 -0
- data/test/dummy/app/controllers/orders/relationships/items_controller.rb +10 -0
- data/test/dummy/app/controllers/orders_controller.rb +38 -0
- data/test/dummy/app/models/order.rb +4 -0
- data/test/dummy/app/serializers/order_serializer.rb +6 -0
- data/test/dummy/config/application.rb +13 -0
- data/test/dummy/config/boot.rb +3 -0
- data/test/dummy/config/database.yml +7 -0
- data/test/dummy/config/environment.rb +5 -0
- data/test/dummy/config/environments/development.rb +44 -0
- data/test/dummy/config/environments/test.rb +44 -0
- data/test/dummy/config/initializers/.keep +0 -0
- data/test/dummy/config/locales/en.yml +2 -0
- data/test/dummy/config/puma.rb +56 -0
- data/test/dummy/config/routes.rb +5 -0
- data/test/dummy/config/secrets.yml +24 -0
- data/test/dummy/config/spring.rb +6 -0
- data/test/dummy/config.ru +5 -0
- data/test/dummy/db/migrate/20170719143227_create_orders.rb +10 -0
- data/test/dummy/db/seeds.rb +7 -0
- data/test/dummy/log/.keep +0 -0
- data/test/dummy/tmp/.keep +0 -0
- data/test/errors/active_model_error_serializer_test.rb +47 -0
- data/test/errors/active_model_error_test.rb +46 -0
- data/test/errors/active_record/record_not_found_serializer_test.rb +33 -0
- data/test/test_helper.rb +35 -0
- metadata +284 -0
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'simple_jsonapi/rails/action_controller/request_validator'
|
2
|
+
|
3
|
+
module SimpleJsonapi
|
4
|
+
module Rails
|
5
|
+
module ActionController
|
6
|
+
class JsonapiHelper
|
7
|
+
attr_reader :controller, :pointers, :request_validator
|
8
|
+
|
9
|
+
delegate :params, :render, :request, :head, to: :controller, allow_nil: true
|
10
|
+
|
11
|
+
def initialize(controller)
|
12
|
+
@controller = controller
|
13
|
+
@pointers = {}
|
14
|
+
@request_validator = RequestValidator.new(request, params)
|
15
|
+
end
|
16
|
+
|
17
|
+
def include_params
|
18
|
+
params[:include].to_s.split(/,/).presence
|
19
|
+
end
|
20
|
+
|
21
|
+
def fields_params
|
22
|
+
(params[:fields] || {}).transform_values { |f| f.split(/,/) }
|
23
|
+
end
|
24
|
+
|
25
|
+
def filter_param(param_name)
|
26
|
+
(params[:filter] || {})[param_name]
|
27
|
+
end
|
28
|
+
|
29
|
+
def filter_param_list(param_name)
|
30
|
+
param_value = filter_param(param_name)
|
31
|
+
return nil unless param_value
|
32
|
+
|
33
|
+
param_value.split(/,/)
|
34
|
+
end
|
35
|
+
|
36
|
+
def sort_related_params
|
37
|
+
(params[:sort_related] || {}).transform_values { |f| f.split(/,/) }
|
38
|
+
end
|
39
|
+
|
40
|
+
# @param error [ActiveRecord::RecordNotFound]
|
41
|
+
def render_record_not_found(error)
|
42
|
+
render jsonapi_errors: error, status: :not_found
|
43
|
+
end
|
44
|
+
|
45
|
+
def render_model_errors(model)
|
46
|
+
errors = SimpleJsonapi::Errors::ActiveModelError.from_errors(model.errors, pointers)
|
47
|
+
render jsonapi_errors: errors, status: :unprocessable_entity
|
48
|
+
end
|
49
|
+
|
50
|
+
def render_bad_request(message)
|
51
|
+
error = SimpleJsonapi::Errors::BadRequest.new(detail: message)
|
52
|
+
render jsonapi_errors: [error], status: :bad_request
|
53
|
+
end
|
54
|
+
|
55
|
+
# private
|
56
|
+
|
57
|
+
# def url_helpers
|
58
|
+
# ::Rails.application.routes.url_helpers
|
59
|
+
# end
|
60
|
+
|
61
|
+
def deserialize(jsonapi_data)
|
62
|
+
jsonapi_hash = case jsonapi_data
|
63
|
+
when String then JSON.parse(jsonapi_data).deep_symbolize_keys
|
64
|
+
when Hash then jsonapi_data.deep_symbolize_keys
|
65
|
+
else jsonapi_data
|
66
|
+
end
|
67
|
+
|
68
|
+
data = jsonapi_hash[:data]
|
69
|
+
return unless data
|
70
|
+
|
71
|
+
result = {}
|
72
|
+
pointers = {}
|
73
|
+
|
74
|
+
result[:type] = data[:type]
|
75
|
+
result[:id] = data[:id]
|
76
|
+
pointers[:type] = "/data/type"
|
77
|
+
pointers[:id] = "/data/id"
|
78
|
+
|
79
|
+
if data[:attributes].present?
|
80
|
+
data[:attributes].each do |name, value|
|
81
|
+
result[name] = value
|
82
|
+
pointers[name] = "/data/attributes/#{name}"
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
if data[:relationships].present?
|
87
|
+
data[:relationships].each do |name, value|
|
88
|
+
related_data = value[:data]
|
89
|
+
|
90
|
+
if related_data.is_a?(Array)
|
91
|
+
singular_name = name.to_s.singularize
|
92
|
+
result[:"#{singular_name}_types"] = related_data.pluck(:type)
|
93
|
+
result[:"#{singular_name}_ids"] = related_data.pluck(:id)
|
94
|
+
pointers[:"#{singular_name}_types"] = "/data/relationships/#{name}"
|
95
|
+
pointers[:"#{name}_ids"] = "/data/relationships/#{name}"
|
96
|
+
elsif related_data.is_a?(Hash)
|
97
|
+
result[:"#{name}_type"] = related_data[:type]
|
98
|
+
result[:"#{name}_id"] = related_data[:id]
|
99
|
+
pointers[:"#{name}_type"] = "/data/relationships/#{name}"
|
100
|
+
pointers[:"#{name}_id"] = "/data/relationships/#{name}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
@pointers = pointers
|
106
|
+
result
|
107
|
+
end
|
108
|
+
|
109
|
+
def validate_jsonapi_request_headers
|
110
|
+
return head :unsupported_media_type unless request_validator.valid_content_type_header?
|
111
|
+
head :not_acceptable unless request_validator.valid_accept_header?
|
112
|
+
end
|
113
|
+
|
114
|
+
def validate_jsonapi_request_body
|
115
|
+
unless request_validator.valid_request_body?
|
116
|
+
raise InvalidJsonStructureError, "Not a valid jsonapi request body"
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
module SimpleJsonapi
|
2
|
+
module Rails
|
3
|
+
module ActionController
|
4
|
+
class RequestValidator
|
5
|
+
attr_reader :request, :params
|
6
|
+
|
7
|
+
delegate :body, :content_type, :accept, :path, to: :request, prefix: true
|
8
|
+
|
9
|
+
def initialize(request, params)
|
10
|
+
@request = request
|
11
|
+
@params = params
|
12
|
+
end
|
13
|
+
|
14
|
+
def valid_content_type_header?
|
15
|
+
!request_has_body? || request_content_type == SimpleJsonapi::MIME_TYPE
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid_accept_header?
|
19
|
+
request_accept.blank? || request_accept == SimpleJsonapi::MIME_TYPE
|
20
|
+
end
|
21
|
+
|
22
|
+
def valid_request_body?
|
23
|
+
return true unless request_has_body?
|
24
|
+
|
25
|
+
params["data"].present? && valid_relationship_body?
|
26
|
+
end
|
27
|
+
|
28
|
+
def valid_relationship_body?
|
29
|
+
request_path.exclude?("relationships") || params["data"]&.is_a?(Array)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def request_has_body?
|
35
|
+
request_body.size > 0
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module SimpleJsonapi
|
2
|
+
module Rails
|
3
|
+
module ActionController
|
4
|
+
extend ActiveSupport::Concern
|
5
|
+
|
6
|
+
delegate :validate_jsonapi_request_headers, :validate_jsonapi_request_body, to: :jsonapi
|
7
|
+
|
8
|
+
included do
|
9
|
+
before_action :validate_jsonapi_request_headers
|
10
|
+
before_action :validate_jsonapi_request_body
|
11
|
+
|
12
|
+
rescue_from ActiveRecord::RecordNotFound do |err|
|
13
|
+
jsonapi.render_record_not_found(err)
|
14
|
+
end
|
15
|
+
|
16
|
+
rescue_from ActiveRecord::RecordInvalid do |err|
|
17
|
+
jsonapi.render_model_errors(err.record)
|
18
|
+
end
|
19
|
+
|
20
|
+
rescue_from ActiveRecord::RecordNotSaved do |err|
|
21
|
+
jsonapi.render_model_errors(err.model)
|
22
|
+
end
|
23
|
+
|
24
|
+
rescue_from InvalidJsonStructureError do |err|
|
25
|
+
jsonapi.render_bad_request(err.message)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class_methods do
|
30
|
+
def jsonapi_deserialize(param_key, options = {})
|
31
|
+
prepend_before_action(options) do
|
32
|
+
if request.raw_post.present?
|
33
|
+
params[param_key] = jsonapi.deserialize(request.request_parameters)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def jsonapi
|
40
|
+
@jsonapi ||= SimpleJsonapi::Rails::ActionController::JsonapiHelper.new(self)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module SimpleJsonapi
|
2
|
+
module Extensions
|
3
|
+
module Routing
|
4
|
+
ACTION_MAP = {
|
5
|
+
add: :create,
|
6
|
+
remove: :destroy,
|
7
|
+
replace: :update,
|
8
|
+
}.freeze
|
9
|
+
|
10
|
+
SUPPORTED_TO_MANY_ACTIONS = ACTION_MAP.keys.freeze
|
11
|
+
|
12
|
+
def jsonapi_to_one_relationship(member_name, association)
|
13
|
+
jsonapi_relationship([:replace], member_name, association)
|
14
|
+
end
|
15
|
+
|
16
|
+
def jsonapi_to_many_relationship(member_name, association, only: nil, except: nil)
|
17
|
+
jsonapi_relationship(to_many_actions_to_define(only, except), member_name, association)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def jsonapi_relationship(actions, member_name, association)
|
23
|
+
member do
|
24
|
+
scope as: member_name, module: member_name.to_s.pluralize do
|
25
|
+
namespace "relationships" do
|
26
|
+
actions.each do |action|
|
27
|
+
resource association, only: [ACTION_MAP[action]], action: action
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_many_actions_to_define(only, except)
|
35
|
+
actions = if only
|
36
|
+
Array(only)
|
37
|
+
elsif except
|
38
|
+
SUPPORTED_TO_MANY_ACTIONS - Array(except)
|
39
|
+
else
|
40
|
+
SUPPORTED_TO_MANY_ACTIONS
|
41
|
+
end
|
42
|
+
|
43
|
+
ensure_actions_supported(actions)
|
44
|
+
|
45
|
+
actions
|
46
|
+
end
|
47
|
+
|
48
|
+
def ensure_actions_supported(actions)
|
49
|
+
if actions.any? { |action| SUPPORTED_TO_MANY_ACTIONS.exclude?(action) }
|
50
|
+
raise ArgumentError, "#jsonapi_to_many_relationship supports :add, :remove, and :replace actions"
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
ActionDispatch::Routing::Mapper.include(SimpleJsonapi::Extensions::Routing)
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'rails/railtie'
|
2
|
+
|
3
|
+
module SimpleJsonapi
|
4
|
+
module Rails
|
5
|
+
class Railtie < ::Rails::Railtie
|
6
|
+
initializer 'simple_jsonapi_rails.initialize' do
|
7
|
+
Mime::Type.register(SimpleJsonapi::MIME_TYPE, :jsonapi)
|
8
|
+
|
9
|
+
ActiveSupport.on_load(:action_controller) do
|
10
|
+
::ActionDispatch::Request.parameter_parsers[:jsonapi] = ->(raw_post) do
|
11
|
+
ActiveSupport::JSON.decode(raw_post)
|
12
|
+
end
|
13
|
+
|
14
|
+
# In the renderers, `self` is the controller
|
15
|
+
|
16
|
+
::ActionController::Renderers.add(:jsonapi_resource) do |resource, options|
|
17
|
+
self.content_type ||= Mime[:jsonapi]
|
18
|
+
SimpleJsonapi.render_resource(resource, options).to_json
|
19
|
+
end
|
20
|
+
|
21
|
+
::ActionController::Renderers.add(:jsonapi_resources) do |resources, options|
|
22
|
+
self.content_type ||= Mime[:jsonapi]
|
23
|
+
SimpleJsonapi.render_resources(resources, options).to_json
|
24
|
+
end
|
25
|
+
|
26
|
+
::ActionController::Renderers.add(:jsonapi_errors) do |errors, options|
|
27
|
+
self.content_type ||= Mime[:jsonapi]
|
28
|
+
SimpleJsonapi.render_errors(errors, options).to_json
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# SimpleJsonapi::Rails::Railtie.initializers.each(&:run)
|
2
|
+
|
3
|
+
# Work-around for rack-test/rack-test#200. Remove once that issue is resolved.
|
4
|
+
module PatchRackTestDeleteRequests
|
5
|
+
def request(uri, env = {}, &block)
|
6
|
+
if env[:method] == :delete && env["HTTP_ACCEPT"] == SimpleJsonapi::MIME_TYPE && JSON.parse(env[:params]).present?
|
7
|
+
env[:input] = env[:params]
|
8
|
+
end
|
9
|
+
|
10
|
+
super(uri, env, &block)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
Rack::Test::Session.prepend(PatchRackTestDeleteRequests)
|
15
|
+
|
16
|
+
ActiveSupport.on_load(:action_controller) do
|
17
|
+
ActionDispatch::IntegrationTest.register_encoder :jsonapi,
|
18
|
+
param_encoder: ->(params) { params.to_json },
|
19
|
+
response_parser: ->(body) { JSON.parse(body) }
|
20
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'active_support'
|
2
|
+
|
3
|
+
require 'simple_jsonapi'
|
4
|
+
|
5
|
+
require 'simple_jsonapi/rails/extensions'
|
6
|
+
require 'simple_jsonapi/rails/extensions/routing'
|
7
|
+
require 'simple_jsonapi/rails/action_controller'
|
8
|
+
require 'simple_jsonapi/rails/action_controller/jsonapi_helper'
|
9
|
+
require 'simple_jsonapi/rails/railtie'
|
10
|
+
|
11
|
+
require 'simple_jsonapi/errors/active_record/record_not_found_serializer'
|
12
|
+
require 'simple_jsonapi/errors/active_model_error'
|
13
|
+
require 'simple_jsonapi/errors/active_model_error_serializer'
|
@@ -0,0 +1,33 @@
|
|
1
|
+
lib = File.expand_path('../lib', __FILE__)
|
2
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
3
|
+
|
4
|
+
require 'simple_jsonapi/rails/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = 'simple_jsonapi_rails'
|
8
|
+
spec.version = SimpleJsonapi::Rails::VERSION
|
9
|
+
spec.license = "MIT"
|
10
|
+
spec.authors = ['PatientsLikeMe']
|
11
|
+
spec.email = ['engineers@patientslikeme.com']
|
12
|
+
spec.homepage = 'https://www.patientslikeme.com'
|
13
|
+
|
14
|
+
spec.summary = 'A library for integrating SimpleJsonapi into a Rails application.'
|
15
|
+
spec.description = 'A library for integrating SimpleJsonapi into a Rails application.'
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0")
|
18
|
+
spec.test_files = spec.files.grep(%r{^test/})
|
19
|
+
spec.require_paths = ['lib']
|
20
|
+
|
21
|
+
spec.add_runtime_dependency 'simple_jsonapi'
|
22
|
+
spec.add_runtime_dependency 'rails', '>= 4.2', '< 6.0'
|
23
|
+
|
24
|
+
spec.add_development_dependency 'bundler', '~> 1.10'
|
25
|
+
spec.add_development_dependency 'listen'
|
26
|
+
spec.add_development_dependency 'minitest'
|
27
|
+
spec.add_development_dependency 'minitest-reporters'
|
28
|
+
spec.add_development_dependency 'pry'
|
29
|
+
spec.add_development_dependency 'rake', '~> 10.0'
|
30
|
+
spec.add_development_dependency 'sqlite3'
|
31
|
+
spec.add_development_dependency 'will_paginate'
|
32
|
+
spec.add_development_dependency 'yard'
|
33
|
+
end
|
@@ -0,0 +1,299 @@
|
|
1
|
+
require_relative 'test_helper'
|
2
|
+
|
3
|
+
class ActionControllerTest < ActionDispatch::IntegrationTest
|
4
|
+
before do
|
5
|
+
@controller = OrdersController.new
|
6
|
+
@routes = Dummy::Application.routes
|
7
|
+
end
|
8
|
+
|
9
|
+
let(:order) { create_order }
|
10
|
+
let(:response_json) { response.parsed_body }
|
11
|
+
|
12
|
+
def create_order
|
13
|
+
Order.create!(customer_name: "Customer X", date: Date.today)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "ActionController" do
|
17
|
+
describe "basic get" do
|
18
|
+
it "gets a collection of resources without parameters" do
|
19
|
+
2.times { create_order }
|
20
|
+
|
21
|
+
get orders_url, as: :jsonapi
|
22
|
+
assert_equal %w[data], response_json.keys
|
23
|
+
assert_equal(%w[orders orders], response_json["data"].map { |o| o["type"] })
|
24
|
+
end
|
25
|
+
|
26
|
+
it "gets a single resource without parameters" do
|
27
|
+
get order_path(order), as: :jsonapi
|
28
|
+
assert_equal %w[data], response_json.keys
|
29
|
+
assert_equal "orders", response_json.dig("data", "type")
|
30
|
+
assert_equal order.id.to_s, response_json.dig("data", "id")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "include parameter" do
|
35
|
+
it "splits a comma-delimited value" do
|
36
|
+
get orders_url(include: "this,that"), as: :jsonapi
|
37
|
+
assert_equal %w[this that], @controller.jsonapi.include_params
|
38
|
+
end
|
39
|
+
|
40
|
+
it "defaults to nil" do
|
41
|
+
get orders_url, as: :jsonapi
|
42
|
+
assert_nil @controller.jsonapi.include_params
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe "fields parameter" do
|
47
|
+
it "splits comma-delimited values" do
|
48
|
+
input = {
|
49
|
+
orders: "customer_name",
|
50
|
+
line_items: "product_name,quantity",
|
51
|
+
}
|
52
|
+
expected_output = {
|
53
|
+
"orders" => %w[customer_name],
|
54
|
+
"line_items" => %w[product_name quantity],
|
55
|
+
}
|
56
|
+
|
57
|
+
get orders_url(fields: input), as: :jsonapi
|
58
|
+
|
59
|
+
assert_equal expected_output, @controller.jsonapi.fields_params.to_unsafe_h
|
60
|
+
end
|
61
|
+
|
62
|
+
it "defaults to an empty hash" do
|
63
|
+
get orders_url, as: :jsonapi
|
64
|
+
assert_equal({}, @controller.jsonapi.fields_params)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "filter_param" do
|
69
|
+
it "fetches the parameter value" do
|
70
|
+
get orders_url("filter[this]" => "that"), as: :jsonapi
|
71
|
+
|
72
|
+
assert_equal "that", @controller.jsonapi.filter_param(:this)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "filter_param_list" do
|
77
|
+
it "splits a comma-delimited string" do
|
78
|
+
get orders_url("filter[these]" => "that,those"), as: :jsonapi
|
79
|
+
|
80
|
+
assert_equal %w[that those], @controller.jsonapi.filter_param_list(:these)
|
81
|
+
end
|
82
|
+
|
83
|
+
it "returns a simple string as an array" do
|
84
|
+
get orders_url("filter[this]" => "that"), as: :jsonapi
|
85
|
+
|
86
|
+
assert_equal %w[that], @controller.jsonapi.filter_param_list(:this)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "sort_related parameter" do
|
91
|
+
it "splits comma-delimited values" do
|
92
|
+
input = {
|
93
|
+
orders: "customer_name",
|
94
|
+
line_items: "product_name,quantity",
|
95
|
+
}
|
96
|
+
expected_output = {
|
97
|
+
"orders" => %w[customer_name],
|
98
|
+
"line_items" => %w[product_name quantity],
|
99
|
+
}
|
100
|
+
|
101
|
+
get orders_url(sort_related: input), as: :jsonapi
|
102
|
+
|
103
|
+
assert_equal expected_output, @controller.jsonapi.sort_related_params.to_unsafe_h
|
104
|
+
end
|
105
|
+
|
106
|
+
it "defaults to an empty hash" do
|
107
|
+
get orders_url, as: :jsonapi
|
108
|
+
assert_equal({}, @controller.jsonapi.fields_params)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
describe "jsonapi_deserialize" do
|
113
|
+
let(:request_json) { request_hash.to_json }
|
114
|
+
let(:request_hash) do
|
115
|
+
{
|
116
|
+
data: {
|
117
|
+
type: "orders",
|
118
|
+
id: "1",
|
119
|
+
attributes: {
|
120
|
+
customer_name: "Jose",
|
121
|
+
date: "2017-10-01",
|
122
|
+
},
|
123
|
+
relationships: {
|
124
|
+
customer: {
|
125
|
+
data: { type: "customers", id: "11" },
|
126
|
+
},
|
127
|
+
products: {
|
128
|
+
data: [
|
129
|
+
{ type: "products", id: "21" },
|
130
|
+
{ type: "widgets", id: "22" },
|
131
|
+
],
|
132
|
+
},
|
133
|
+
},
|
134
|
+
},
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
let(:helper) { SimpleJsonapi::Rails::ActionController::JsonapiHelper.new(nil) }
|
139
|
+
let(:deserialized) { helper.deserialize(request_hash) }
|
140
|
+
|
141
|
+
it "parses the request body" do
|
142
|
+
post orders_url, params: request_hash, as: :jsonapi
|
143
|
+
assert_response :created
|
144
|
+
assert_instance_of ActionController::Parameters, @controller.params[:order]
|
145
|
+
assert_equal "orders", @controller.params.dig(:order, :type)
|
146
|
+
assert_equal "1", @controller.params.dig(:order, :id)
|
147
|
+
end
|
148
|
+
|
149
|
+
it "is a no-op if there's no request body" do
|
150
|
+
2.times { create_order }
|
151
|
+
|
152
|
+
get orders_url, as: :jsonapi
|
153
|
+
assert_response :ok
|
154
|
+
assert_nil @controller.params[:order]
|
155
|
+
end
|
156
|
+
|
157
|
+
it "moves the type and id to the object param" do
|
158
|
+
assert_equal "orders", deserialized[:type]
|
159
|
+
assert_equal "1", deserialized[:id]
|
160
|
+
end
|
161
|
+
|
162
|
+
it "moves the attributes to the object param" do
|
163
|
+
assert_equal "Jose", deserialized[:customer_name]
|
164
|
+
assert_equal "2017-10-01", deserialized[:date]
|
165
|
+
end
|
166
|
+
|
167
|
+
it "moves singular relationships to the object param" do
|
168
|
+
assert_equal "customers", deserialized[:customer_type]
|
169
|
+
assert_equal "11", deserialized[:customer_id]
|
170
|
+
end
|
171
|
+
|
172
|
+
it "moves collection relationships to the object param" do
|
173
|
+
assert_equal %w[products widgets], deserialized[:product_types]
|
174
|
+
assert_equal %w[21 22], deserialized[:product_ids]
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
describe "rendering errors" do
|
179
|
+
it "renders ActiveModel::Errors" do
|
180
|
+
post orders_url, params: {
|
181
|
+
data: {
|
182
|
+
type: "orders",
|
183
|
+
attributes: {
|
184
|
+
customer_name: "",
|
185
|
+
date: Date.today.iso8601,
|
186
|
+
},
|
187
|
+
},
|
188
|
+
}, as: :jsonapi
|
189
|
+
|
190
|
+
expected_output = {
|
191
|
+
errors: [{
|
192
|
+
status: "422",
|
193
|
+
code: "unprocessable_entity",
|
194
|
+
title: "Invalid customer_name",
|
195
|
+
detail: "Customer name can't be blank",
|
196
|
+
source: { pointer: "/data/attributes/customer_name" },
|
197
|
+
},],
|
198
|
+
}.deep_stringify_keys
|
199
|
+
|
200
|
+
assert_response :unprocessable_entity
|
201
|
+
assert_equal expected_output, response_json
|
202
|
+
end
|
203
|
+
|
204
|
+
it "renders ActiveRecord::RecordNotFound" do
|
205
|
+
get order_path(-1), as: :jsonapi
|
206
|
+
|
207
|
+
expected_output = {
|
208
|
+
errors: [{
|
209
|
+
status: "404",
|
210
|
+
code: "not_found",
|
211
|
+
title: "Not found",
|
212
|
+
detail: "Couldn't find Order with 'id'=-1",
|
213
|
+
source: { parameter: "id" },
|
214
|
+
},],
|
215
|
+
}.deep_stringify_keys
|
216
|
+
|
217
|
+
assert_response :not_found
|
218
|
+
assert_equal expected_output, response_json
|
219
|
+
end
|
220
|
+
end
|
221
|
+
|
222
|
+
describe "routing" do
|
223
|
+
it "generates correct relationship paths" do
|
224
|
+
assert_generates "/orders/1/relationships/items",
|
225
|
+
controller: "orders/relationships/items",
|
226
|
+
action: "add",
|
227
|
+
id: 1
|
228
|
+
|
229
|
+
assert_generates "/orders/1/relationships/items",
|
230
|
+
controller: "orders/relationships/items",
|
231
|
+
action: "remove",
|
232
|
+
id: 1
|
233
|
+
|
234
|
+
assert_generates "/orders/1/relationships/items",
|
235
|
+
controller: "orders/relationships/items",
|
236
|
+
action: "replace",
|
237
|
+
id: 1
|
238
|
+
end
|
239
|
+
|
240
|
+
it "generates the correct path helpers" do
|
241
|
+
assert_equal "/orders/1/relationships/items", orders_relationships_items_path(1)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
describe "request validation" do
|
246
|
+
let(:request_hash) do
|
247
|
+
{
|
248
|
+
data: {
|
249
|
+
type: "orders",
|
250
|
+
attributes: {
|
251
|
+
customer_name: "Jose",
|
252
|
+
},
|
253
|
+
},
|
254
|
+
}
|
255
|
+
end
|
256
|
+
|
257
|
+
let(:jsonapi_mime_type) do
|
258
|
+
SimpleJsonapi::MIME_TYPE
|
259
|
+
end
|
260
|
+
|
261
|
+
it "returns a 406 if there is an accept header that does not match the required mime-type" do
|
262
|
+
get order_path(order), headers: { "Accept" => "application/json" }
|
263
|
+
|
264
|
+
assert_response :not_acceptable
|
265
|
+
end
|
266
|
+
|
267
|
+
# NB: This doesn't quite test what happens if there is no accept header, since Rails not so helpfully adds in an
|
268
|
+
# Accept header if none is present.
|
269
|
+
it "does not require an accept header" do
|
270
|
+
get order_path(order), headers: { "Accept" => "" }
|
271
|
+
|
272
|
+
assert_response :ok
|
273
|
+
end
|
274
|
+
|
275
|
+
it "returns a 415 if there is a request body but not the proper Content Type header" do
|
276
|
+
headers = {
|
277
|
+
"Accept" => jsonapi_mime_type,
|
278
|
+
"Content-Type" => "application/json",
|
279
|
+
}
|
280
|
+
|
281
|
+
post orders_url, params: request_hash.to_json, headers: headers
|
282
|
+
|
283
|
+
assert_response :unsupported_media_type
|
284
|
+
end
|
285
|
+
|
286
|
+
it "returns a 400 if there is no data element" do
|
287
|
+
post orders_url, params: { hinkle: "finkle_dinkle_doo" }, as: :jsonapi
|
288
|
+
|
289
|
+
assert_response :bad_request
|
290
|
+
end
|
291
|
+
|
292
|
+
it "returns a 400 if the request is to a relationship and there is not a data array" do
|
293
|
+
post orders_relationships_items_url(1), params: { data: "array non est" }, as: :jsonapi
|
294
|
+
|
295
|
+
assert_response :bad_request
|
296
|
+
end
|
297
|
+
end
|
298
|
+
end
|
299
|
+
end
|