lp-serializable 0.1.0 → 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.
@@ -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: []