grape-jsonapi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +7 -0
  2. data/.circleci/config.yml +65 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +15 -0
  5. data/CHANGELOG.md +52 -0
  6. data/Gemfile +8 -0
  7. data/Gemfile.lock +219 -0
  8. data/LICENSE +21 -0
  9. data/README.md +88 -0
  10. data/Rakefile +3 -0
  11. data/grape-jsonapi.gemspec +29 -0
  12. data/lib/grape_jsonapi.rb +11 -0
  13. data/lib/grape_jsonapi/deprecated/formatter.rb +25 -0
  14. data/lib/grape_jsonapi/deprecated/parser.rb +21 -0
  15. data/lib/grape_jsonapi/endpoint_extension.rb +12 -0
  16. data/lib/grape_jsonapi/formatter.rb +92 -0
  17. data/lib/grape_jsonapi/parser.rb +195 -0
  18. data/lib/grape_jsonapi/version.rb +7 -0
  19. data/spec/lib/grape_jsonapi/deprecated.rb/formatter_spec.rb +7 -0
  20. data/spec/lib/grape_jsonapi/deprecated.rb/parser_spec.rb +7 -0
  21. data/spec/lib/grape_jsonapi/formatter_spec.rb +121 -0
  22. data/spec/lib/grape_jsonapi/parser_spec.rb +214 -0
  23. data/spec/lib/grape_jsonapi/version_spec.rb +5 -0
  24. data/spec/spec_helper.rb +6 -0
  25. data/spec/support/models/blog_post.rb +17 -0
  26. data/spec/support/models/db_record.rb +19 -0
  27. data/spec/support/models/foo.rb +28 -0
  28. data/spec/support/models/user.rb +27 -0
  29. data/spec/support/models/user_admin.rb +31 -0
  30. data/spec/support/serializers/another_blog_post_serializer.rb +9 -0
  31. data/spec/support/serializers/another_user_serializer.rb +7 -0
  32. data/spec/support/serializers/blog_post_serializer.rb +9 -0
  33. data/spec/support/serializers/db_record_serializer.rb +18 -0
  34. data/spec/support/serializers/foo_serializer.rb +23 -0
  35. data/spec/support/serializers/user_serializer.rb +9 -0
  36. metadata +165 -0
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
@@ -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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module EndpointExtension
5
+ def render(resources, options = {})
6
+ env['jsonapi_serializer_options'] = options
7
+ resources
8
+ end
9
+ end
10
+
11
+ Endpoint.include EndpointExtension
12
+ 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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Grape
4
+ module Jsonapi
5
+ VERSION = '1.0.0'
6
+ end
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe Grape::Formatter::FastJsonapi do
4
+ subject { described_class }
5
+
6
+ it { expect(subject.deprecated?).to be true }
7
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ describe GrapeSwagger::FastJsonapi::Parser do
4
+ subject { described_class }
5
+
6
+ it { expect(subject.deprecated?).to be true }
7
+ 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