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.
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