fast_jsonapi 1.0.17 → 1.1.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 (38) hide show
  1. checksums.yaml +5 -5
  2. data/README.md +106 -38
  3. data/lib/extensions/has_one.rb +19 -13
  4. data/lib/fast_jsonapi.rb +2 -0
  5. data/lib/fast_jsonapi/instrumentation.rb +2 -0
  6. data/lib/fast_jsonapi/instrumentation/serializable_hash.rb +15 -0
  7. data/lib/fast_jsonapi/instrumentation/serialized_json.rb +15 -0
  8. data/lib/fast_jsonapi/instrumentation/skylight.rb +2 -0
  9. data/lib/fast_jsonapi/instrumentation/skylight/normalizers/serializable_hash.rb +22 -0
  10. data/lib/fast_jsonapi/instrumentation/skylight/normalizers/serialized_json.rb +22 -0
  11. data/lib/fast_jsonapi/multi_to_json.rb +92 -0
  12. data/lib/fast_jsonapi/object_serializer.rb +120 -91
  13. data/lib/fast_jsonapi/serialization_core.rb +44 -32
  14. data/lib/generators/serializer/USAGE +8 -0
  15. data/lib/generators/serializer/serializer_generator.rb +19 -0
  16. data/lib/generators/serializer/templates/serializer.rb.tt +6 -0
  17. metadata +48 -88
  18. data/.document +0 -5
  19. data/.rspec +0 -1
  20. data/Gemfile +0 -4
  21. data/Gemfile.lock +0 -158
  22. data/README.rdoc +0 -231
  23. data/Rakefile +0 -55
  24. data/VERSION +0 -1
  25. data/docs/collection_serializer_output.json +0 -35
  26. data/docs/object_serializer.json +0 -30
  27. data/fast_jsonapi.gemspec +0 -108
  28. data/spec/lib/extensions/active_record_spec.rb +0 -67
  29. data/spec/lib/object_serializer_caching_spec.rb +0 -68
  30. data/spec/lib/object_serializer_class_methods_spec.rb +0 -69
  31. data/spec/lib/object_serializer_hyphen_spec.rb +0 -40
  32. data/spec/lib/object_serializer_performance_spec.rb +0 -87
  33. data/spec/lib/object_serializer_spec.rb +0 -126
  34. data/spec/lib/object_serializer_struct_spec.rb +0 -31
  35. data/spec/lib/serialization_core_spec.rb +0 -84
  36. data/spec/shared/contexts/ams_context.rb +0 -83
  37. data/spec/shared/contexts/movie_context.rb +0 -192
  38. data/spec/spec_helper.rb +0 -15
@@ -1,76 +1,60 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/core_ext/object'
2
4
  require 'active_support/concern'
3
5
  require 'active_support/inflector'
4
- require 'oj'
5
- require 'multi_json'
6
6
  require 'fast_jsonapi/serialization_core'
7
7
 
8
- begin
9
- require 'skylight'
10
- SKYLIGHT_ENABLED = true
11
- rescue LoadError
12
- SKYLIGHT_ENABLED = false
13
- end
14
-
15
8
  module FastJsonapi
16
9
  module ObjectSerializer
17
10
  extend ActiveSupport::Concern
18
11
  include SerializationCore
19
12
 
20
- included do
21
- # Skylight integration
22
- # To remove Skylight
23
- # Remove the included do block
24
- # Remove the Gemfile entry
25
- if SKYLIGHT_ENABLED
26
- include Skylight::Helpers
27
-
28
- instrument_method :serializable_hash
29
- instrument_method :to_json
30
- end
13
+ SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'.freeze
14
+ SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'.freeze
31
15
 
16
+ included do
32
17
  # Set record_type based on the name of the serializer class
33
- set_type default_record_type if default_record_type
18
+ set_type(reflected_record_type) if reflected_record_type
34
19
  end
35
20
 
36
21
  def initialize(resource, options = {})
37
- if options.present?
38
- @meta_tags = options[:meta]
39
- @includes = options[:include].delete_if(&:blank?) if options[:include].present?
40
- self.class.has_permitted_includes(@includes) if @includes.present?
41
- @known_included_objects = {} # keep track of inc objects that have already been serialized
42
- end
43
- # @records if enumerables like Array, ActiveRecord::Relation but if Struct just make it a @record
44
- if resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
45
- @records = resource
46
- else
47
- @record = resource
48
- end
22
+ process_options(options)
23
+
24
+ @resource = resource
49
25
  end
50
26
 
51
27
  def serializable_hash
52
- serializable_hash = { data: nil }
53
- serializable_hash[:meta] = @meta_tags if @meta_tags.present?
54
- return hash_for_one_record(serializable_hash) if @record
55
- return hash_for_multiple_records(serializable_hash) if @records
56
- serializable_hash
28
+ return hash_for_collection if is_collection?(@resource)
29
+
30
+ hash_for_one_record
57
31
  end
32
+ alias_method :to_hash, :serializable_hash
33
+
34
+ def hash_for_one_record
35
+ serializable_hash = { data: nil }
36
+ serializable_hash[:meta] = @meta if @meta.present?
37
+
38
+ return serializable_hash unless @resource
58
39
 
59
- def hash_for_one_record(serializable_hash)
60
- serializable_hash[:data] = self.class.record_hash(@record)
61
- serializable_hash[:included] = self.class.get_included_records(@record, @includes, @known_included_objects) if @includes.present?
40
+ serializable_hash[:data] = self.class.record_hash(@resource)
41
+ serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects) if @includes.present?
62
42
  serializable_hash
63
43
  end
64
44
 
65
- def hash_for_multiple_records(serializable_hash)
45
+ def hash_for_collection
46
+ serializable_hash = {}
47
+
66
48
  data = []
67
49
  included = []
68
- @records.each do |record|
50
+ @resource.each do |record|
69
51
  data << self.class.record_hash(record)
70
52
  included.concat self.class.get_included_records(record, @includes, @known_included_objects) if @includes.present?
71
53
  end
54
+
72
55
  serializable_hash[:data] = data
73
56
  serializable_hash[:included] = included if @includes.present?
57
+ serializable_hash[:meta] = @meta if @meta.present?
74
58
  serializable_hash
75
59
  end
76
60
 
@@ -78,24 +62,74 @@ module FastJsonapi
78
62
  self.class.to_json(serializable_hash)
79
63
  end
80
64
 
65
+ private
66
+
67
+ def process_options(options)
68
+ return if options.blank?
69
+
70
+ @known_included_objects = {}
71
+ @meta = options[:meta]
72
+
73
+ if options[:include].present?
74
+ @includes = options[:include].delete_if(&:blank?).map(&:to_sym)
75
+ validate_includes!(@includes)
76
+ end
77
+ end
78
+
79
+ def validate_includes!(includes)
80
+ return if includes.blank?
81
+
82
+ existing_relationships = self.class.relationships_to_serialize.keys.to_set
83
+
84
+ unless existing_relationships.superset?(includes.to_set)
85
+ raise ArgumentError, "One of keys from #{includes} is not specified as a relationship on the serializer"
86
+ end
87
+ end
88
+
89
+ def is_collection?(resource)
90
+ resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
91
+ end
92
+
81
93
  class_methods do
94
+ def reflected_record_type
95
+ return @reflected_record_type if defined?(@reflected_record_type)
96
+
97
+ @reflected_record_type ||= begin
98
+ if self.name.end_with?('Serializer')
99
+ self.name.split('::').last.chomp('Serializer').underscore.to_sym
100
+ end
101
+ end
102
+ end
103
+
104
+ def set_key_transform(transform_name)
105
+ mapping = {
106
+ camel: :camelize,
107
+ camel_lower: [:camelize, :lower],
108
+ dash: :dasherize,
109
+ underscore: :underscore
110
+ }
111
+ @transform_method = mapping[transform_name.to_sym]
112
+ end
113
+
114
+ def run_key_transform(input)
115
+ if @transform_method.present?
116
+ input.to_s.send(*@transform_method).to_sym
117
+ else
118
+ input.to_sym
119
+ end
120
+ end
121
+
82
122
  def use_hyphen
83
- @hyphenated = true
123
+ warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead')
124
+ set_key_transform :dash
84
125
  end
85
126
 
86
127
  def set_type(type_name)
87
- self.record_type = type_name
88
- if @hyphenated
89
- self.record_type = type_name.to_s.dasherize.to_sym
90
- end
128
+ self.record_type = run_key_transform(type_name)
91
129
  end
92
130
 
93
- def default_record_type
94
- if self.name.end_with?('Serializer')
95
- class_name = self.name.demodulize
96
- range_end = class_name.rindex('Serializer')
97
- class_name[0...range_end].underscore.to_sym
98
- end
131
+ def set_id(id_name)
132
+ self.record_id = id_name
99
133
  end
100
134
 
101
135
  def cache_options(cache_options)
@@ -103,19 +137,18 @@ module FastJsonapi
103
137
  self.cache_length = cache_options[:cache_length] || 5.minutes
104
138
  end
105
139
 
106
- def attributes(*attributes_list)
140
+ def attributes(*attributes_list, &block)
107
141
  attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
108
142
  self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
109
143
  attributes_list.each do |attr_name|
110
144
  method_name = attr_name
111
- key = method_name
112
- if @hyphenated
113
- key = attr_name.to_s.dasherize.to_sym
114
- end
115
- attributes_to_serialize[key] = method_name
145
+ key = run_key_transform(method_name)
146
+ attributes_to_serialize[key] = block || method_name
116
147
  end
117
148
  end
118
149
 
150
+ alias_method :attribute, :attributes
151
+
119
152
  def add_relationship(name, relationship)
120
153
  self.relationships_to_serialize = {} if relationships_to_serialize.nil?
121
154
  self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
@@ -130,15 +163,11 @@ module FastJsonapi
130
163
  end
131
164
 
132
165
  def has_many(relationship_name, options = {})
133
- singular_name = relationship_name.to_s.singularize
134
- record_type = options[:record_type] || singular_name.to_sym
135
166
  name = relationship_name.to_sym
136
- key = options[:key] || name
137
- if @hyphenated
138
- key = options[:key] || relationship_name.to_s.dasherize.to_sym
139
- record_type = options[:record_type] || singular_name.to_s.dasherize.to_sym
140
- end
141
- serializer_key = options[:serializer] || record_type
167
+ singular_name = relationship_name.to_s.singularize
168
+ serializer_key = options[:serializer] || singular_name.to_sym
169
+ key = options[:key] || run_key_transform(relationship_name)
170
+ record_type = options[:record_type] || run_key_transform(singular_name)
142
171
  relationship = {
143
172
  key: key,
144
173
  name: name,
@@ -147,21 +176,18 @@ module FastJsonapi
147
176
  object_method_name: options[:object_method_name] || name,
148
177
  serializer: compute_serializer_name(serializer_key),
149
178
  relationship_type: :has_many,
150
- cached: options[:cached] || false
179
+ cached: options[:cached] || false,
180
+ polymorphic: fetch_polymorphic_option(options)
151
181
  }
152
182
  add_relationship(name, relationship)
153
183
  end
154
184
 
155
185
  def belongs_to(relationship_name, options = {})
156
186
  name = relationship_name.to_sym
157
- key = options[:key] || name
158
- record_type = options[:record_type] || name
159
- serializer_key = options[:serializer] || record_type
160
- if @hyphenated
161
- key = options[:key] || relationship_name.to_s.dasherize.to_sym
162
- record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym
163
- end
164
- relationship = {
187
+ serializer_key = options[:serializer] || relationship_name.to_sym
188
+ key = options[:key] || run_key_transform(relationship_name)
189
+ record_type = options[:record_type] || run_key_transform(relationship_name)
190
+ add_relationship(name, {
165
191
  key: key,
166
192
  name: name,
167
193
  id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym,
@@ -169,21 +195,17 @@ module FastJsonapi
169
195
  object_method_name: options[:object_method_name] || name,
170
196
  serializer: compute_serializer_name(serializer_key),
171
197
  relationship_type: :belongs_to,
172
- cached: options[:cached] || true
173
- }
174
- add_relationship(name, relationship)
198
+ cached: options[:cached] || true,
199
+ polymorphic: fetch_polymorphic_option(options)
200
+ })
175
201
  end
176
202
 
177
203
  def has_one(relationship_name, options = {})
178
204
  name = relationship_name.to_sym
179
- key = options[:key] || name
180
- record_type = options[:record_type] || name
181
- serializer_key = options[:serializer] || record_type
182
- if @hyphenated
183
- key = options[:key] || relationship_name.to_s.dasherize.to_sym
184
- record_type = options[:record_type] || relationship_name.to_s.dasherize.to_sym
185
- end
186
- relationship = {
205
+ serializer_key = options[:serializer] || name
206
+ key = options[:key] || run_key_transform(relationship_name)
207
+ record_type = options[:record_type] || run_key_transform(relationship_name)
208
+ add_relationship(name, {
187
209
  key: key,
188
210
  name: name,
189
211
  id_method_name: options[:id_method_name] || (relationship_name.to_s + '_id').to_sym,
@@ -191,9 +213,9 @@ module FastJsonapi
191
213
  object_method_name: options[:object_method_name] || name,
192
214
  serializer: compute_serializer_name(serializer_key),
193
215
  relationship_type: :has_one,
194
- cached: options[:cached] || false
195
- }
196
- add_relationship(name, relationship)
216
+ cached: options[:cached] || false,
217
+ polymorphic: fetch_polymorphic_option(options)
218
+ })
197
219
  end
198
220
 
199
221
  def compute_serializer_name(serializer_key)
@@ -202,6 +224,13 @@ module FastJsonapi
202
224
  return (namespace + serializer_name).to_sym if namespace.present?
203
225
  (serializer_key.to_s.classify + 'Serializer').to_sym
204
226
  end
227
+
228
+ def fetch_polymorphic_option(options)
229
+ option = options[:polymorphic]
230
+ return false unless option.present?
231
+ return option if option.respond_to? :keys
232
+ {}
233
+ end
205
234
  end
206
235
  end
207
236
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require 'active_support/concern'
4
+ require 'fast_jsonapi/multi_to_json'
2
5
 
3
6
  module FastJsonapi
4
7
  module SerializationCore
@@ -11,6 +14,7 @@ module FastJsonapi
11
14
  :cachable_relationships_to_serialize,
12
15
  :uncachable_relationships_to_serialize,
13
16
  :record_type,
17
+ :record_id,
14
18
  :cache_length,
15
19
  :cached
16
20
  end
@@ -26,58 +30,78 @@ module FastJsonapi
26
30
  id_hash(ids, record_type) # ids variable is just a single id here
27
31
  end
28
32
 
33
+ def id_hash_from_record(record, record_types)
34
+ # memoize the record type within the record_types dictionary, then assigning to record_type:
35
+ record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
36
+ { id: record.id.to_s, type: record_type }
37
+ end
38
+
39
+ def ids_hash_from_record_and_relationship(record, relationship)
40
+ polymorphic = relationship[:polymorphic]
41
+
42
+ return ids_hash(
43
+ record.public_send(relationship[:id_method_name]),
44
+ relationship[:record_type]
45
+ ) unless polymorphic
46
+
47
+ object_method_name = relationship.fetch(:object_method_name, relationship[:name])
48
+ return unless associated_object = record.send(object_method_name)
49
+
50
+ return associated_object.map do |object|
51
+ id_hash_from_record object, polymorphic
52
+ end if associated_object.respond_to? :map
53
+
54
+ id_hash_from_record associated_object, polymorphic
55
+ end
56
+
29
57
  def attributes_hash(record)
30
- attributes_hash = {}
31
- attributes_to_serialize.each do |key, method_name|
32
- attributes_hash[key] = record.send(method_name)
58
+ attributes_to_serialize.each_with_object({}) do |(key, method), attr_hash|
59
+ attr_hash[key] = method.is_a?(Proc) ? method.call(record) : record.public_send(method)
33
60
  end
34
- attributes_hash
35
61
  end
36
62
 
37
63
  def relationships_hash(record, relationships = nil)
38
- relationships_hash = {}
39
64
  relationships = relationships_to_serialize if relationships.nil?
40
65
 
41
- relationships.each do |_k, relationship|
66
+ relationships.each_with_object({}) do |(_k, relationship), hash|
42
67
  name = relationship[:key]
43
- id_method_name = relationship[:id_method_name]
44
- record_type = relationship[:record_type]
45
68
  empty_case = relationship[:relationship_type] == :has_many ? [] : nil
46
- relationships_hash[name] = {
47
- data: ids_hash(record.send(id_method_name), record_type) || empty_case
69
+ hash[name] = {
70
+ data: ids_hash_from_record_and_relationship(record, relationship) || empty_case
48
71
  }
49
72
  end
50
- relationships_hash
51
73
  end
52
74
 
53
75
  def record_hash(record)
54
76
  if cached
55
77
  record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length) do
56
- record_hash = id_hash(record.id, record_type) || { id: nil, type: record_type }
57
- record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
58
- record_hash[:relationships] = {}
59
- record_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present?
60
- record_hash
78
+ id = record_id ? record.send(record_id) : record.id
79
+ temp_hash = id_hash(id, record_type) || { id: nil, type: record_type }
80
+ temp_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
81
+ temp_hash[:relationships] = {}
82
+ temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize) if cachable_relationships_to_serialize.present?
83
+ temp_hash
61
84
  end
62
85
  record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize)) if uncachable_relationships_to_serialize.present?
63
86
  record_hash
64
87
  else
65
- record_hash = id_hash(record.id, record_type) || { id: nil, type: record_type }
88
+ id = record_id ? record.send(record_id) : record.id
89
+ record_hash = id_hash(id, record_type) || { id: nil, type: record_type }
66
90
  record_hash[:attributes] = attributes_hash(record) if attributes_to_serialize.present?
67
91
  record_hash[:relationships] = relationships_hash(record) if relationships_to_serialize.present?
68
92
  record_hash
69
93
  end
70
94
  end
71
95
 
96
+ # Override #to_json for alternative implementation
72
97
  def to_json(payload)
73
- MultiJson.dump(payload) if payload.present?
98
+ FastJsonapi::MultiToJson.to_json(payload) if payload.present?
74
99
  end
75
100
 
76
101
  # includes handler
77
102
 
78
103
  def get_included_records(record, includes_list, known_included_objects)
79
- included_records = []
80
- includes_list.each do |item|
104
+ includes_list.each_with_object([]) do |item, included_records|
81
105
  object_method_name = @relationships_to_serialize[item][:object_method_name]
82
106
  record_type = @relationships_to_serialize[item][:record_type]
83
107
  serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
@@ -92,18 +116,6 @@ module FastJsonapi
92
116
  included_records << serializer.record_hash(inc_obj)
93
117
  end
94
118
  end
95
- included_records
96
- end
97
-
98
- def has_permitted_includes(requested_includes)
99
- # requested includes should be within relationships defined on serializer
100
- allowed_includes = @relationships_to_serialize.keys
101
- intersection = allowed_includes & requested_includes
102
- if intersection.sort == requested_includes.sort
103
- true
104
- else
105
- raise ArgumentError, "One of keys from #{requested_includes} is not specified as a relationship on the serializer"
106
- end
107
119
  end
108
120
  end
109
121
  end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates a serializer for the given model.
3
+
4
+ Example:
5
+ rails generate serializer Movie
6
+
7
+ This will create:
8
+ app/serializers/movie_serializer.rb