fast_jsonapi 1.0.17 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
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