lp-serializable 0.1.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/concern'
4
+ require 'fast_jsonapi/multi_to_json'
5
+
6
+ module FastJsonapi
7
+ MandatoryField = Class.new(StandardError)
8
+
9
+ module SerializationCore
10
+ extend ActiveSupport::Concern
11
+
12
+ included do
13
+ class << self
14
+ attr_accessor :attributes_to_serialize,
15
+ :relationships_to_serialize,
16
+ :cachable_relationships_to_serialize,
17
+ :uncachable_relationships_to_serialize,
18
+ :transform_method,
19
+ :record_type,
20
+ :record_id,
21
+ :cache_length,
22
+ :race_condition_ttl,
23
+ :cached,
24
+ :data_links
25
+ end
26
+ end
27
+
28
+ class_methods do
29
+ def id_hash(id, record_type, default_return=false)
30
+ if id.present?
31
+ { id: id.to_s, type: record_type }
32
+ else
33
+ default_return ? { id: nil, type: record_type } : nil
34
+ end
35
+ end
36
+
37
+ def ids_hash(ids, record_type)
38
+ return ids.map { |id| id_hash(id, record_type) } if ids.respond_to? :map
39
+ id_hash(ids, record_type) # ids variable is just a single id here
40
+ end
41
+
42
+ def id_hash_from_record(record, record_types)
43
+ # memoize the record type within the record_types dictionary, then assigning to record_type:
44
+ record_type = record_types[record.class] ||= record.class.name.underscore.to_sym
45
+ id_hash(record.id, record_type)
46
+ end
47
+
48
+ def ids_hash_from_record_and_relationship(record, relationship, params = {})
49
+ polymorphic = relationship[:polymorphic]
50
+
51
+ return ids_hash(
52
+ fetch_id(record, relationship, params),
53
+ relationship[:record_type]
54
+ ) unless polymorphic
55
+
56
+ return unless associated_object = fetch_associated_object(record, relationship, params)
57
+
58
+ return associated_object.map do |object|
59
+ id_hash_from_record object, polymorphic
60
+ end if associated_object.respond_to? :map
61
+
62
+ id_hash_from_record associated_object, polymorphic
63
+ end
64
+
65
+ def links_hash(record, params = {})
66
+ data_links.each_with_object({}) do |(key, method), link_hash|
67
+ link_hash[key] = if method.is_a?(Proc)
68
+ method.arity == 1 ? method.call(record) : method.call(record, params)
69
+ else
70
+ record.public_send(method)
71
+ end
72
+ end
73
+ end
74
+
75
+ def attributes_hash(record, params = {})
76
+ attributes = attributes_to_serialize
77
+ attributes.each_with_object({}) do |(_k, attribute), hash|
78
+ attribute.serialize(record, params, hash)
79
+ end
80
+ end
81
+
82
+ def relationships_hash(record, relationships = nil, params = {})
83
+ relationships = relationships_to_serialize if relationships.nil?
84
+
85
+ relationships.each_with_object({}) do |(_k, relationship), hash|
86
+ name = relationship[:key]
87
+ empty_case = relationship[:relationship_type] == :has_many ? [] : nil
88
+ hash[name] = {
89
+ data: ids_hash_from_record_and_relationship(record, relationship, params) || empty_case
90
+ }
91
+ end
92
+ end
93
+
94
+ def record_hash(record, params = {})
95
+ if cached
96
+ record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do
97
+ temp_hash = id_hash(id_from_record(record), record_type, true)
98
+ temp_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present?
99
+ temp_hash[:relationships] = {}
100
+ temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, params) if cachable_relationships_to_serialize.present?
101
+ temp_hash[:links] = links_hash(record, params) if data_links.present?
102
+ temp_hash
103
+ end
104
+ record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, params)) if uncachable_relationships_to_serialize.present?
105
+ record_hash
106
+ else
107
+ record_hash = id_hash(id_from_record(record), record_type, true)
108
+ record_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present?
109
+ record_hash[:relationships] = relationships_hash(record, nil, params) if relationships_to_serialize.present?
110
+ record_hash[:links] = links_hash(record, params) if data_links.present?
111
+ record_hash
112
+ end
113
+ end
114
+
115
+ def id_from_record(record)
116
+ return record.send(record_id) if record_id
117
+ raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
118
+ record.id
119
+ end
120
+
121
+ # Override #to_json for alternative implementation
122
+ def to_json(payload)
123
+ FastJsonapi::MultiToJson.to_json(payload) if payload.present?
124
+ end
125
+
126
+ def parse_include_item(include_item)
127
+ return [include_item.to_sym] unless include_item.to_s.include?('.')
128
+ include_item.to_s.split('.').map { |item| item.to_sym }
129
+ end
130
+
131
+ def remaining_items(items)
132
+ return unless items.size > 1
133
+
134
+ items_copy = items.dup
135
+ items_copy.delete_at(0)
136
+ [items_copy.join('.').to_sym]
137
+ end
138
+
139
+ # includes handler
140
+ def get_included_records(record, includes_list, known_included_objects, params = {})
141
+ return unless includes_list.present?
142
+
143
+ includes_list.sort.each_with_object([]) do |include_item, included_records|
144
+ items = parse_include_item(include_item)
145
+ items.each do |item|
146
+ next unless relationships_to_serialize && relationships_to_serialize[item]
147
+ raise NotImplementedError if @relationships_to_serialize[item][:polymorphic].is_a?(Hash)
148
+ record_type = @relationships_to_serialize[item][:record_type]
149
+ serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
150
+ relationship_type = @relationships_to_serialize[item][:relationship_type]
151
+
152
+ included_objects = fetch_associated_object(record, @relationships_to_serialize[item], params)
153
+ next if included_objects.blank?
154
+ included_objects = [included_objects] unless relationship_type == :has_many
155
+
156
+ included_objects.each do |inc_obj|
157
+ if remaining_items(items)
158
+ serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects)
159
+ included_records.concat(serializer_records) unless serializer_records.empty?
160
+ end
161
+
162
+ code = "#{record_type}_#{inc_obj.id}"
163
+ next if known_included_objects.key?(code)
164
+
165
+ known_included_objects[code] = inc_obj
166
+ included_records << serializer.record_hash(inc_obj, params)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+ def fetch_associated_object(record, relationship, params)
173
+ return relationship[:object_block].call(record, params) unless relationship[:object_block].nil?
174
+ record.send(relationship[:object_method_name])
175
+ end
176
+
177
+ def fetch_id(record, relationship, params)
178
+ unless relationship[:object_block].nil?
179
+ object = relationship[:object_block].call(record, params)
180
+
181
+ return object.map(&:id) if object.respond_to? :map
182
+ return object.id
183
+ end
184
+
185
+ record.public_send(relationship[:id_method_name])
186
+ end
187
+ end
188
+ end
189
+ end
@@ -1,32 +1,44 @@
1
- require "lp/serializable/version"
2
1
  require "lp/serializable/strategies"
2
+ require "lp/serializable/exceptions"
3
3
 
4
4
  module Lp
5
5
  module Serializable
6
6
  include Strategies
7
+ include Exceptions
7
8
 
8
- class UnserializableCollection < StandardError
9
- def initialize(msg='Expected an object, but a collection was given.')
10
- super
11
- end
12
- end
13
-
14
- def serialize_and_flatten(resource, options={})
15
- base_hash = serialize_hash(resource)
9
+ def serialize_and_flatten(resource, options = {})
10
+ collection_option = collection?(false)
11
+ base_hash = serialize_hash(resource, options.merge(collection_option))
16
12
  flatten_and_nest_data(base_hash, set_nested_option(options))
17
13
  end
18
14
 
19
- def serialize_and_flatten_with_class_name(resource, class_name, options={})
20
- raise UnserializableCollection if resource.is_a?(Array)
21
- base_hash = serializable_hash_with_class_name(resource,
22
- class_name, options)
15
+ def serialize_and_flatten_with_class_name(
16
+ resource,
17
+ class_name,
18
+ options = {}
19
+ )
20
+ raise UnserializableCollection if resource.is_a?(Array)
21
+ collection_option = collection?(false)
22
+ base_hash = serializable_hash_with_class_name(
23
+ resource,
24
+ class_name,
25
+ options.merge(collection_option),
26
+ )
23
27
  flatten_and_nest_data(base_hash, set_nested_option(options))
24
28
  end
25
29
 
26
- def serialize_and_flatten_collection(resource, class_name, options={})
27
- base_hash = serializable_hash_with_class_name(resource,
28
- class_name, options)
30
+ def serialize_and_flatten_collection(resource, class_name, options = {})
31
+ collection_option = collection?(true)
32
+ base_hash = serializable_hash_with_class_name(
33
+ resource,
34
+ class_name,
35
+ options.merge(collection_option),
36
+ )
29
37
  flatten_array_and_nest_data(base_hash, set_nested_option(options))
30
38
  end
39
+
40
+ alias_method :serializable, :serialize_and_flatten
41
+ alias_method :serializable_class, :serialize_and_flatten_with_class_name
42
+ alias_method :serializable_collection, :serialize_and_flatten_collection
31
43
  end
32
- end
44
+ end
@@ -0,0 +1,11 @@
1
+ module Lp
2
+ module Serializable
3
+ module Exceptions
4
+ class UnserializableCollection < StandardError
5
+ def initialize(msg = "Expected an object, but a collection was given.")
6
+ super
7
+ end
8
+ end
9
+ end
10
+ end
11
+ end
@@ -9,14 +9,14 @@ module Lp
9
9
 
10
10
  private
11
11
 
12
- def serialize_hash(resource, options={})
13
- "#{resource.class.name}Serializer"
14
- .constantize.new(resource, options).serializable_hash
12
+ def serialize_hash(resource, options = {})
13
+ "#{resource.class.name}Serializer".
14
+ constantize.new(resource, options).serializable_hash
15
15
  end
16
16
 
17
- def serializable_hash_with_class_name(resource, class_name, options={})
18
- "#{class_name}Serializer"
19
- .constantize.new(resource, options).serializable_hash
17
+ def serializable_hash_with_class_name(resource, class_name, options = {})
18
+ "#{class_name}Serializer".
19
+ constantize.new(resource, options).serializable_hash
20
20
  end
21
21
 
22
22
  def flatten_and_nest_data(hash, nested)
@@ -26,10 +26,14 @@ module Lp
26
26
  def flatten_array_and_nest_data(hash, nested)
27
27
  nest_data?(flatten_array_of_hashes(expose_data(hash)), nested)
28
28
  end
29
-
29
+
30
+ def collection?(boolean)
31
+ { is_collection: boolean }
32
+ end
33
+
30
34
  def set_nested_option(options)
31
35
  options.fetch(:nested, false)
32
36
  end
33
37
  end
34
38
  end
35
- end
39
+ end
@@ -3,6 +3,8 @@ module Lp
3
3
  module Utilities
4
4
  private
5
5
 
6
+ REDUNDANT_KEYS = %i[attributes relationships].freeze
7
+
6
8
  def nest_data?(resource, nested)
7
9
  if nested
8
10
  resource
@@ -33,16 +35,31 @@ module Lp
33
35
 
34
36
  def flatten_hash(hash)
35
37
  return unless hash
36
- hash.each_with_object({}) do |(k, v), h|
37
- if v.is_a?(Hash) && k == :attributes
38
- flatten_hash(v).map do |h_k, h_v|
39
- h["#{h_k}".to_sym] = h_v
40
- end
41
- else
42
- h[k] = v
38
+ hash.each_with_object({}) do |(key, value), h|
39
+ if hash_and_matches_redundant_keys?(key, value)
40
+ flatten_hash_map(value, h)
41
+ elsif hash_and_has_data_key?(value)
42
+ h[key] = expose_data(value)
43
+ else
44
+ h[key] = value
43
45
  end
44
46
  end
45
47
  end
48
+
49
+ def flatten_hash_map(value, hash)
50
+ flatten_hash(value).map do |h_k, h_v|
51
+ hash[h_k.to_s.to_sym] = h_v
52
+ end
53
+ end
54
+
55
+ def hash_and_has_data_key?(value)
56
+ value.is_a?(Hash) && value.key?(:data)
57
+ end
58
+
59
+ # NOTE Supports native relationship references in serializer
60
+ def hash_and_matches_redundant_keys?(key, value)
61
+ value.is_a?(Hash) && REDUNDANT_KEYS.any? { |sym| sym == key }
62
+ end
46
63
  end
47
64
  end
48
- end
65
+ end
@@ -1,5 +1,5 @@
1
1
  module Lp
2
2
  module Serializable
3
- VERSION = "0.1.0"
3
+ VERSION = "1.0.0"
4
4
  end
5
- end
5
+ end
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["mrjonesbot"]
10
10
  spec.email = ["nate@mrjones.io"]
11
11
 
12
- spec.summary = "Serialize with Fast JSON API, flatten like AWS"
13
- # spec.description = %q{TODO: Write a longer description or delete this line.}
12
+ spec.summary = "Serialize with Fast JSON API, flatten like AMS"
13
+ spec.description = "JSON API(jsonapi.org) serializer wrapper methods that work with Rails and can be used to serialize any kind of ruby object with AMS style output."
14
14
  spec.homepage = "https://www.github.com/launchpadlab/lp-serializable"
15
15
  spec.license = "MIT"
16
16
 
@@ -35,6 +35,15 @@ Gem::Specification.new do |spec|
35
35
  spec.add_development_dependency "rake", "~> 10.0"
36
36
  spec.add_development_dependency "rspec", "~> 3.0"
37
37
  spec.add_development_dependency "pry"
38
+ spec.add_development_dependency(%q<activerecord>, [">= 4.2"])
39
+ spec.add_development_dependency(%q<active_model_serializers>, ["~> 0.10.7"])
40
+ spec.add_development_dependency(%q<jsonapi-rb>, ["~> 0.5.0"])
41
+ spec.add_development_dependency(%q<sqlite3>, ["~> 1.3"])
42
+ spec.add_development_dependency(%q<jsonapi-serializers>, ["~> 1.0.0"])
43
+ spec.add_development_dependency(%q<oj>, ["~> 3.3"])
44
+ spec.add_development_dependency(%q<rspec-benchmark>, ["~> 0.3.0"])
45
+ spec.add_runtime_dependency(%q<activesupport>, [">= 4.2"])
46
+ spec.add_development_dependency(%q<byebug>, [">= 0"])
38
47
 
39
- spec.add_dependency "fast_jsonapi"
48
+ spec.add_dependency "fast_jsonapi", '>= 1.3'
40
49
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lp-serializable
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mrjonesbot
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-07-09 00:00:00.000000000 Z
11
+ date: 2020-07-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: appraisal
@@ -81,20 +81,147 @@ dependencies:
81
81
  - !ruby/object:Gem::Version
82
82
  version: '0'
83
83
  - !ruby/object:Gem::Dependency
84
- name: fast_jsonapi
84
+ name: activerecord
85
85
  requirement: !ruby/object:Gem::Requirement
86
86
  requirements:
87
87
  - - ">="
88
88
  - !ruby/object:Gem::Version
89
- version: '0'
89
+ version: '4.2'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '4.2'
97
+ - !ruby/object:Gem::Dependency
98
+ name: active_model_serializers
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.10.7
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.10.7
111
+ - !ruby/object:Gem::Dependency
112
+ name: jsonapi-rb
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - "~>"
116
+ - !ruby/object:Gem::Version
117
+ version: 0.5.0
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - "~>"
123
+ - !ruby/object:Gem::Version
124
+ version: 0.5.0
125
+ - !ruby/object:Gem::Dependency
126
+ name: sqlite3
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.3'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.3'
139
+ - !ruby/object:Gem::Dependency
140
+ name: jsonapi-serializers
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - "~>"
144
+ - !ruby/object:Gem::Version
145
+ version: 1.0.0
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - "~>"
151
+ - !ruby/object:Gem::Version
152
+ version: 1.0.0
153
+ - !ruby/object:Gem::Dependency
154
+ name: oj
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - "~>"
158
+ - !ruby/object:Gem::Version
159
+ version: '3.3'
160
+ type: :development
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - "~>"
165
+ - !ruby/object:Gem::Version
166
+ version: '3.3'
167
+ - !ruby/object:Gem::Dependency
168
+ name: rspec-benchmark
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - "~>"
172
+ - !ruby/object:Gem::Version
173
+ version: 0.3.0
174
+ type: :development
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - "~>"
179
+ - !ruby/object:Gem::Version
180
+ version: 0.3.0
181
+ - !ruby/object:Gem::Dependency
182
+ name: activesupport
183
+ requirement: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - ">="
186
+ - !ruby/object:Gem::Version
187
+ version: '4.2'
90
188
  type: :runtime
91
189
  prerelease: false
190
+ version_requirements: !ruby/object:Gem::Requirement
191
+ requirements:
192
+ - - ">="
193
+ - !ruby/object:Gem::Version
194
+ version: '4.2'
195
+ - !ruby/object:Gem::Dependency
196
+ name: byebug
197
+ requirement: !ruby/object:Gem::Requirement
198
+ requirements:
199
+ - - ">="
200
+ - !ruby/object:Gem::Version
201
+ version: '0'
202
+ type: :development
203
+ prerelease: false
92
204
  version_requirements: !ruby/object:Gem::Requirement
93
205
  requirements:
94
206
  - - ">="
95
207
  - !ruby/object:Gem::Version
96
208
  version: '0'
97
- description:
209
+ - !ruby/object:Gem::Dependency
210
+ name: fast_jsonapi
211
+ requirement: !ruby/object:Gem::Requirement
212
+ requirements:
213
+ - - ">="
214
+ - !ruby/object:Gem::Version
215
+ version: '1.3'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '1.3'
223
+ description: JSON API(jsonapi.org) serializer wrapper methods that work with Rails
224
+ and can be used to serialize any kind of ruby object with AMS style output.
98
225
  email:
99
226
  - nate@mrjones.io
100
227
  executables: []
@@ -103,6 +230,8 @@ extra_rdoc_files: []
103
230
  files:
104
231
  - ".gitignore"
105
232
  - ".rspec"
233
+ - ".rubocop.yml"
234
+ - ".ruby-version"
106
235
  - ".travis.yml"
107
236
  - CODE_OF_CONDUCT.md
108
237
  - Gemfile
@@ -112,7 +241,11 @@ files:
112
241
  - Rakefile
113
242
  - bin/console
114
243
  - bin/setup
244
+ - lib/fast_jsonapi/multi_to_json.rb
245
+ - lib/fast_jsonapi/object_serializer.rb
246
+ - lib/fast_jsonapi/serialization_core.rb
115
247
  - lib/lp/serializable.rb
248
+ - lib/lp/serializable/exceptions.rb
116
249
  - lib/lp/serializable/strategies.rb
117
250
  - lib/lp/serializable/utilities.rb
118
251
  - lib/lp/serializable/version.rb
@@ -140,5 +273,5 @@ rubyforge_project:
140
273
  rubygems_version: 2.7.3
141
274
  signing_key:
142
275
  specification_version: 4
143
- summary: Serialize with Fast JSON API, flatten like AWS
276
+ summary: Serialize with Fast JSON API, flatten like AMS
144
277
  test_files: []