grape-jsonapi 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/.circleci/config.yml +65 -0
- data/.rspec +3 -0
- data/.rubocop.yml +15 -0
- data/CHANGELOG.md +52 -0
- data/Gemfile +8 -0
- data/Gemfile.lock +219 -0
- data/LICENSE +21 -0
- data/README.md +88 -0
- data/Rakefile +3 -0
- data/grape-jsonapi.gemspec +29 -0
- data/lib/grape_jsonapi.rb +11 -0
- data/lib/grape_jsonapi/deprecated/formatter.rb +25 -0
- data/lib/grape_jsonapi/deprecated/parser.rb +21 -0
- data/lib/grape_jsonapi/endpoint_extension.rb +12 -0
- data/lib/grape_jsonapi/formatter.rb +92 -0
- data/lib/grape_jsonapi/parser.rb +195 -0
- data/lib/grape_jsonapi/version.rb +7 -0
- data/spec/lib/grape_jsonapi/deprecated.rb/formatter_spec.rb +7 -0
- data/spec/lib/grape_jsonapi/deprecated.rb/parser_spec.rb +7 -0
- data/spec/lib/grape_jsonapi/formatter_spec.rb +121 -0
- data/spec/lib/grape_jsonapi/parser_spec.rb +214 -0
- data/spec/lib/grape_jsonapi/version_spec.rb +5 -0
- data/spec/spec_helper.rb +6 -0
- data/spec/support/models/blog_post.rb +17 -0
- data/spec/support/models/db_record.rb +19 -0
- data/spec/support/models/foo.rb +28 -0
- data/spec/support/models/user.rb +27 -0
- data/spec/support/models/user_admin.rb +31 -0
- data/spec/support/serializers/another_blog_post_serializer.rb +9 -0
- data/spec/support/serializers/another_user_serializer.rb +7 -0
- data/spec/support/serializers/blog_post_serializer.rb +9 -0
- data/spec/support/serializers/db_record_serializer.rb +18 -0
- data/spec/support/serializers/foo_serializer.rb +23 -0
- data/spec/support/serializers/user_serializer.rb +9 -0
- metadata +165 -0
data/Rakefile
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
# Generated by EmCousin
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require File.expand_path('lib/grape_jsonapi/version', __dir__)
|
5
|
+
|
6
|
+
Gem::Specification.new do |gem|
|
7
|
+
gem.authors = ['Emmanuel Cousin']
|
8
|
+
gem.email = ['emmanuel_cousin@hotmail.fr']
|
9
|
+
gem.summary = 'Use grape-jsonapi in grape'
|
10
|
+
gem.description = 'Provides a Formatter for the Grape API DSL to emit objects serialized with jsonapi-serializer.'
|
11
|
+
gem.homepage = 'https://github.com/EmCousin/grape-jsonapi'
|
12
|
+
|
13
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
14
|
+
gem.files = `git ls-files`.split("\n")
|
15
|
+
gem.test_files = `git ls-files -- {test,spec}/*`.split("\n")
|
16
|
+
gem.name = 'grape-jsonapi'
|
17
|
+
gem.require_paths = ['lib']
|
18
|
+
gem.version = Grape::Jsonapi::VERSION
|
19
|
+
gem.licenses = ['MIT']
|
20
|
+
|
21
|
+
gem.required_ruby_version = '>= 2.6.0'
|
22
|
+
|
23
|
+
gem.add_dependency 'grape'
|
24
|
+
gem.add_dependency 'jsonapi-serializer'
|
25
|
+
|
26
|
+
gem.add_development_dependency 'rails', '>= 4.2.0'
|
27
|
+
gem.add_development_dependency 'rspec', '~> 3.7'
|
28
|
+
gem.add_development_dependency 'rubocop'
|
29
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'jsonapi/serializer'
|
4
|
+
require 'grape'
|
5
|
+
require 'grape_jsonapi/endpoint_extension'
|
6
|
+
require 'grape_jsonapi/formatter'
|
7
|
+
require 'grape_jsonapi/parser'
|
8
|
+
require 'grape_jsonapi/version'
|
9
|
+
|
10
|
+
require 'grape_jsonapi/deprecated/formatter'
|
11
|
+
require 'grape_jsonapi/deprecated/parser'
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Formatter
|
5
|
+
module FastJsonapi
|
6
|
+
include Grape::Formatter::Jsonapi
|
7
|
+
|
8
|
+
class << self
|
9
|
+
def call(object, env)
|
10
|
+
warn "
|
11
|
+
WARNING: Grape::Formatter::FastJsonapi is deprecated
|
12
|
+
and will be removed on version 1.1
|
13
|
+
Use Grape::Formatter::Jsonapi instead
|
14
|
+
"
|
15
|
+
|
16
|
+
super(object, env)
|
17
|
+
end
|
18
|
+
|
19
|
+
def deprecated?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,21 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrapeSwagger
|
4
|
+
module FastJsonapi
|
5
|
+
class Parser < GrapeSwagger::Jsonapi::Parser
|
6
|
+
def initialize(model, endpoint)
|
7
|
+
warn "
|
8
|
+
WARNING: Grape::FastJsonapi::Parser is deprecated
|
9
|
+
and will be removed on version 1.1
|
10
|
+
Use Grape::Jsonapi::Parser instead
|
11
|
+
"
|
12
|
+
|
13
|
+
super(model, endpoint)
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.deprecated?
|
17
|
+
true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,92 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Grape
|
4
|
+
module Formatter
|
5
|
+
module Jsonapi
|
6
|
+
class << self
|
7
|
+
def call(object, env)
|
8
|
+
return object if object.is_a?(String)
|
9
|
+
return ::Grape::Json.dump(serialize(object, env)) if serializable?(object)
|
10
|
+
return object.to_json if object.respond_to?(:to_json)
|
11
|
+
|
12
|
+
::Grape::Json.dump(object)
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def serializable?(object)
|
18
|
+
return false if object.nil?
|
19
|
+
return true if object.respond_to?(:serializable_hash)
|
20
|
+
return true if object.is_a?(Hash)
|
21
|
+
|
22
|
+
serializable_collection?(object)
|
23
|
+
end
|
24
|
+
|
25
|
+
def serialize(object, env)
|
26
|
+
if object.respond_to?(:serializable_hash)
|
27
|
+
serializable_object(object, jsonapi_options(env)).serializable_hash
|
28
|
+
elsif serializable_collection?(object)
|
29
|
+
serializable_collection(object, jsonapi_options(env))
|
30
|
+
elsif object.is_a?(Hash)
|
31
|
+
serialize_each_pair(object, env)
|
32
|
+
else
|
33
|
+
object
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def serializable_collection?(object)
|
38
|
+
object.respond_to?(:to_a) && object.all? do |o|
|
39
|
+
o.respond_to?(:serializable_hash)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def serializable_object(object, options)
|
44
|
+
jsonapi_serializable(object, options) || object
|
45
|
+
end
|
46
|
+
|
47
|
+
def jsonapi_serializable(object, options)
|
48
|
+
serializable_class(object, options)&.new(object, options)
|
49
|
+
end
|
50
|
+
|
51
|
+
def serializable_collection(collection, options)
|
52
|
+
if heterogeneous_collection?(collection)
|
53
|
+
collection.map do |o|
|
54
|
+
serialize_resource(o, options)
|
55
|
+
end
|
56
|
+
else
|
57
|
+
serialize_resource(collection, options)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
def heterogeneous_collection?(collection)
|
62
|
+
collection.map { |item| item.class.name }.uniq.many?
|
63
|
+
end
|
64
|
+
|
65
|
+
def serialize_resource(resource, options)
|
66
|
+
jsonapi_serializable(resource, options)&.serializable_hash || resource.map(&:serializable_hash)
|
67
|
+
end
|
68
|
+
|
69
|
+
def serializable_class(object, options)
|
70
|
+
klass_name = options['serializer'] || options[:serializer]
|
71
|
+
klass_name ||= begin
|
72
|
+
object = object.first if object.is_a?(Array)
|
73
|
+
|
74
|
+
"#{(object.try(:model_name) || object.class).name}Serializer"
|
75
|
+
end
|
76
|
+
|
77
|
+
klass_name&.safe_constantize
|
78
|
+
end
|
79
|
+
|
80
|
+
def serialize_each_pair(object, env)
|
81
|
+
h = {}
|
82
|
+
object.each_pair { |k, v| h[k] = serialize(v, env) }
|
83
|
+
h
|
84
|
+
end
|
85
|
+
|
86
|
+
def jsonapi_options(env)
|
87
|
+
env['jsonapi_serializer_options'] || {}
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
@@ -0,0 +1,195 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module GrapeSwagger
|
4
|
+
module Jsonapi
|
5
|
+
class Parser
|
6
|
+
attr_reader :model, :endpoint
|
7
|
+
|
8
|
+
def initialize(model, endpoint)
|
9
|
+
@model = model
|
10
|
+
@endpoint = endpoint
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
schema = default_schema
|
15
|
+
schema = enrich_with_attributes(schema)
|
16
|
+
schema = enrich_with_relationships(schema)
|
17
|
+
schema.deep_merge!(model.additional_schema) if model.respond_to?(:additional_schema)
|
18
|
+
|
19
|
+
schema
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def default_schema
|
25
|
+
{ data: {
|
26
|
+
type: :object,
|
27
|
+
properties: default_schema_propeties,
|
28
|
+
example: {
|
29
|
+
id: 1,
|
30
|
+
type: model.record_type,
|
31
|
+
attributes: {},
|
32
|
+
relationships: {}
|
33
|
+
}
|
34
|
+
} }
|
35
|
+
end
|
36
|
+
|
37
|
+
def default_schema_propeties
|
38
|
+
{ id: { type: :integer },
|
39
|
+
type: { type: :string },
|
40
|
+
attributes: default_schema_object,
|
41
|
+
relationships: default_schema_object }
|
42
|
+
end
|
43
|
+
|
44
|
+
def default_schema_object
|
45
|
+
{ type: :object, properties: {} }
|
46
|
+
end
|
47
|
+
|
48
|
+
def enrich_with_attributes(schema)
|
49
|
+
attributes_hash.each do |attribute, type|
|
50
|
+
schema[:data][:properties][:attributes][:properties][attribute] = { type: type }
|
51
|
+
example_method = "#{type}_example"
|
52
|
+
unless respond_to?(example_method, true)
|
53
|
+
puts "WARN unexpected type encountered, missing #{example_method} --use string example instead"
|
54
|
+
example_method = 'string_example'
|
55
|
+
end
|
56
|
+
schema[:data][:example][:attributes][attribute] = send(example_method)
|
57
|
+
end
|
58
|
+
|
59
|
+
schema
|
60
|
+
end
|
61
|
+
|
62
|
+
def attributes_hash
|
63
|
+
return map_model_attributes.symbolize_keys unless defined?(ActiveRecord)
|
64
|
+
|
65
|
+
map_model_attributes.symbolize_keys.merge(
|
66
|
+
map_active_record_columns_to_attributes.symbolize_keys
|
67
|
+
)
|
68
|
+
end
|
69
|
+
|
70
|
+
def enrich_with_relationships(schema)
|
71
|
+
relationships_hash.each do |model_type, relationship_data|
|
72
|
+
relationships_attributes = relationship_data.instance_values.symbolize_keys
|
73
|
+
schema[:data][:properties][:relationships][:properties][model_type] = {
|
74
|
+
type: :object,
|
75
|
+
properties: relationships_properties(relationships_attributes)
|
76
|
+
}
|
77
|
+
schema[:data][:example][:relationships][model_type] = relationships_example(relationships_attributes)
|
78
|
+
end
|
79
|
+
|
80
|
+
schema
|
81
|
+
end
|
82
|
+
|
83
|
+
def relationships_hash
|
84
|
+
hash = model.relationships_to_serialize || []
|
85
|
+
|
86
|
+
# If relationship has :key set different than association name, it should be rendered under that key
|
87
|
+
|
88
|
+
hash.each_with_object({}) do |(_relationship_name, relationship), accu|
|
89
|
+
accu[relationship.key] = relationship
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def map_active_record_columns_to_attributes
|
94
|
+
return map_model_attributes unless activerecord_model && activerecord_model < ActiveRecord::Base
|
95
|
+
|
96
|
+
activerecord_model.columns.each_with_object({}) do |column, attributes|
|
97
|
+
next unless model.attributes_to_serialize.key?(column.name.to_sym)
|
98
|
+
|
99
|
+
attributes[column.name] = column.type
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def activerecord_model
|
104
|
+
model.record_type.to_s.camelize.safe_constantize
|
105
|
+
end
|
106
|
+
|
107
|
+
def map_model_attributes
|
108
|
+
attributes = {}
|
109
|
+
(model.attributes_to_serialize || []).each do |attribute, _|
|
110
|
+
attributes[attribute] =
|
111
|
+
if model.respond_to? :attribute_types
|
112
|
+
model.attribute_types[attribute] || :string
|
113
|
+
else
|
114
|
+
:string
|
115
|
+
end
|
116
|
+
end
|
117
|
+
attributes
|
118
|
+
end
|
119
|
+
|
120
|
+
def relationships_properties(relationship_data)
|
121
|
+
return { data: RELATIONSHIP_DEFAULT_ITEM } unless relationship_data[:relationship_type] == :has_many
|
122
|
+
|
123
|
+
{ data: {
|
124
|
+
type: :array,
|
125
|
+
items: RELATIONSHIP_DEFAULT_ITEM
|
126
|
+
} }
|
127
|
+
end
|
128
|
+
|
129
|
+
RELATIONSHIP_DEFAULT_ITEM = {
|
130
|
+
type: :object,
|
131
|
+
properties: {
|
132
|
+
id: { type: :integer },
|
133
|
+
type: { type: :string }
|
134
|
+
}
|
135
|
+
}.freeze
|
136
|
+
|
137
|
+
def relationships_example(relationship_data)
|
138
|
+
data = {
|
139
|
+
id: 1,
|
140
|
+
type: relationship_data[:record_type] ||
|
141
|
+
relationship_data[:static_record_type] ||
|
142
|
+
relationship_data[:object_method_name]
|
143
|
+
}
|
144
|
+
|
145
|
+
data = [data] if relationship_data[:relationship_type] == :has_many
|
146
|
+
|
147
|
+
{ data: data }
|
148
|
+
end
|
149
|
+
|
150
|
+
def integer_example
|
151
|
+
defined?(Faker) ? Faker::Number.number.to_i : rand(1..9999)
|
152
|
+
end
|
153
|
+
|
154
|
+
def string_example
|
155
|
+
defined?(Faker) ? Faker::Lorem.word : 'Example string'
|
156
|
+
end
|
157
|
+
|
158
|
+
def text_example
|
159
|
+
defined?(Faker) ? Faker::Lorem.paragraph : 'Example string'
|
160
|
+
end
|
161
|
+
alias citext_example text_example
|
162
|
+
|
163
|
+
def float_example
|
164
|
+
rand * rand(1..100)
|
165
|
+
end
|
166
|
+
|
167
|
+
def date_example
|
168
|
+
Date.today.to_s
|
169
|
+
end
|
170
|
+
|
171
|
+
def datetime_example
|
172
|
+
Time.current.to_s
|
173
|
+
end
|
174
|
+
alias time_example datetime_example
|
175
|
+
|
176
|
+
def object_example
|
177
|
+
return { example: :object } unless defined?(Faker)
|
178
|
+
|
179
|
+
{ string_example.parameterize.underscore.to_sym => string_example.parameterize.underscore.to_sym }
|
180
|
+
end
|
181
|
+
|
182
|
+
def array_example
|
183
|
+
[string_example]
|
184
|
+
end
|
185
|
+
|
186
|
+
def boolean_example
|
187
|
+
[true, false].sample
|
188
|
+
end
|
189
|
+
|
190
|
+
def uuid_example
|
191
|
+
SecureRandom.uuid
|
192
|
+
end
|
193
|
+
end
|
194
|
+
end
|
195
|
+
end
|
@@ -0,0 +1,121 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
describe Grape::Formatter::Jsonapi do
|
4
|
+
describe 'class methods' do
|
5
|
+
let(:user) do
|
6
|
+
User.new(id: 1, first_name: 'Chuck', last_name: 'Norris', password: 'supersecretpassword', email: 'chuck@norris.com')
|
7
|
+
end
|
8
|
+
let(:another_user) do
|
9
|
+
User.new(id: 2, first_name: 'Bruce', last_name: 'Lee', password: 'supersecretpassword', email: 'bruce@lee.com')
|
10
|
+
end
|
11
|
+
let(:blog_post) do
|
12
|
+
BlogPost.new(id: 1, title: 'Blog Post title', body: 'Blog post body')
|
13
|
+
end
|
14
|
+
let(:admin) do
|
15
|
+
UserAdmin.new(id: 1, first_name: 'Jean Luc', last_name: 'Picard', password: 'supersecretpassword', email: 'jeanluc@picard.com')
|
16
|
+
end
|
17
|
+
|
18
|
+
describe '.call' do
|
19
|
+
subject { described_class.call(object, env) }
|
20
|
+
let(:jsonapi_serializer_options) { nil }
|
21
|
+
let(:env) { { 'jsonapi_serializer_options' => jsonapi_serializer_options } }
|
22
|
+
|
23
|
+
context 'when the object is a string' do
|
24
|
+
let(:object) { 'I am a string' }
|
25
|
+
|
26
|
+
it { is_expected.to eq object }
|
27
|
+
end
|
28
|
+
|
29
|
+
context 'when the object is serializable' do
|
30
|
+
let(:user_serializer) { UserSerializer.new(object, {}) }
|
31
|
+
let(:another_user_serializer) { AnotherUserSerializer.new(object, {}) }
|
32
|
+
let(:blog_post_serializer) { BlogPostSerializer.new(object, {}) }
|
33
|
+
|
34
|
+
context 'when the object has a model_name defined' do
|
35
|
+
let(:object) { admin }
|
36
|
+
it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
|
37
|
+
end
|
38
|
+
|
39
|
+
context 'when the object is a active serializable model instance' do
|
40
|
+
let(:object) { user }
|
41
|
+
|
42
|
+
it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
|
43
|
+
end
|
44
|
+
|
45
|
+
context 'when the object is an array of active serializable model instances' do
|
46
|
+
let(:object) { [user, another_user] }
|
47
|
+
|
48
|
+
it { is_expected.to eq ::Grape::Json.dump(user_serializer.serializable_hash) }
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'when the array contains instances of different models' do
|
52
|
+
let(:object) { [user, blog_post] }
|
53
|
+
|
54
|
+
it 'returns an array of jsonapi serialialized objects' do
|
55
|
+
expect(subject).to eq(::Grape::Json.dump([
|
56
|
+
UserSerializer.new(user, {}).serializable_hash,
|
57
|
+
BlogPostSerializer.new(blog_post, {}).serializable_hash
|
58
|
+
]))
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
context 'when the object is an empty array ' do
|
63
|
+
let(:object) { [] }
|
64
|
+
|
65
|
+
it { is_expected.to eq ::Grape::Json.dump(object) }
|
66
|
+
end
|
67
|
+
|
68
|
+
context 'when the object is an array of null objects ' do
|
69
|
+
let(:object) { [nil, nil] }
|
70
|
+
|
71
|
+
it { is_expected.to eq ::Grape::Json.dump(object) }
|
72
|
+
end
|
73
|
+
|
74
|
+
context 'when the object is a Hash of plain values' do
|
75
|
+
let(:object) { user.as_json }
|
76
|
+
|
77
|
+
it { is_expected.to eq ::Grape::Json.dump(object) }
|
78
|
+
end
|
79
|
+
|
80
|
+
context 'when the object is a Hash with serializable object values' do
|
81
|
+
let(:object) do
|
82
|
+
{
|
83
|
+
user: user,
|
84
|
+
blog_post: blog_post
|
85
|
+
}
|
86
|
+
end
|
87
|
+
|
88
|
+
it 'returns an hash of with jsonapi serialialized objects values' do
|
89
|
+
expect(subject).to eq(::Grape::Json.dump({
|
90
|
+
user: UserSerializer.new(user, {}).serializable_hash,
|
91
|
+
blog_post: BlogPostSerializer.new(blog_post, {}).serializable_hash
|
92
|
+
}))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
context 'when the object is nil' do
|
97
|
+
let(:object) { nil }
|
98
|
+
|
99
|
+
it { is_expected.to eq 'null' }
|
100
|
+
end
|
101
|
+
|
102
|
+
context 'when the object is a number' do
|
103
|
+
let(:object) { 42 }
|
104
|
+
|
105
|
+
it { is_expected.to eq '42' }
|
106
|
+
end
|
107
|
+
|
108
|
+
context 'when a custom serializer is passed as an option' do
|
109
|
+
let(:object) { user }
|
110
|
+
let(:jsonapi_serializer_options) do
|
111
|
+
{
|
112
|
+
'serializer' => '::AnotherUserSerializer'
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
it { is_expected.to eq ::Grape::Json.dump(another_user_serializer.serializable_hash) }
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|