simple_jsonapi_rails 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|