lp-serializable 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9d1c00efce38a2b3b5333b36c47fd57256eef4caf70c4d873f4bc8483bbe64db
4
- data.tar.gz: b54707da56b2e33bae82dd790ff8db046cce3ce4bc7c6ab44b176013a2c53699
3
+ metadata.gz: ee8c8007befe5042222082f7d355d28d8b5364d0a06499416055c7ff9c2ba24c
4
+ data.tar.gz: 9eeba9747a87355b25f45c013131374be16ff9c2f4a600c55efbf806f93148e1
5
5
  SHA512:
6
- metadata.gz: 0c58d890a8aba1bdc6d8811db5fb68c04012d1a926d36c97748324e793afc451135d4b060d5e9300f5c8ec2cb8ca96a0256d4591275c2a88be5c482bbaf666c0
7
- data.tar.gz: 6a8f7c983b80a7e8aa4a149750dfcc3198a4f0348b5ccad65c024dbcbfef788eb3cc7e511f3efdf71dbecee70372493e9b7da1c87b601bf60fad115341397781
6
+ metadata.gz: 8ec72124b13225234bfcdedb2663f52a5fa474ede7c4a526303d937098e60a1bd7c33342e2e0cfba9f805eb99d82171a9e6e08fb2820b428940ddb5c792febc8
7
+ data.tar.gz: 9d3899a92c259237da9bdca814bcc7ce00938764cd08ec889028466ec83100fba4e5ddd0a7fd9160aed2f4f788bc862ee1123270c9cdce0af7eab9e892485705
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.5.0
data/.travis.yml CHANGED
@@ -3,3 +3,5 @@ language: ruby
3
3
  rvm:
4
4
  - 2.5.0
5
5
  before_install: gem install bundler -v 1.16.1
6
+ ignore:
7
+ - spec/lib/object_serializer_performance.rb
data/Gemfile.lock CHANGED
@@ -1,12 +1,37 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- lp-serializable (0.1.0)
5
- fast_jsonapi
4
+ lp-serializable (0.2.0)
5
+ activesupport (>= 4.2)
6
+ fast_jsonapi (>= 1.2)
6
7
 
7
8
  GEM
8
9
  remote: https://rubygems.org/
9
10
  specs:
11
+ actionpack (5.2.0)
12
+ actionview (= 5.2.0)
13
+ activesupport (= 5.2.0)
14
+ rack (~> 2.0)
15
+ rack-test (>= 0.6.3)
16
+ rails-dom-testing (~> 2.0)
17
+ rails-html-sanitizer (~> 1.0, >= 1.0.2)
18
+ actionview (5.2.0)
19
+ activesupport (= 5.2.0)
20
+ builder (~> 3.1)
21
+ erubi (~> 1.4)
22
+ rails-dom-testing (~> 2.0)
23
+ rails-html-sanitizer (~> 1.0, >= 1.0.3)
24
+ active_model_serializers (0.10.7)
25
+ actionpack (>= 4.1, < 6)
26
+ activemodel (>= 4.1, < 6)
27
+ case_transform (>= 0.2)
28
+ jsonapi-renderer (>= 0.1.1.beta1, < 0.3)
29
+ activemodel (5.2.0)
30
+ activesupport (= 5.2.0)
31
+ activerecord (5.2.0)
32
+ activemodel (= 5.2.0)
33
+ activesupport (= 5.2.0)
34
+ arel (>= 9.0)
10
35
  activesupport (5.2.0)
11
36
  concurrent-ruby (~> 1.0, >= 1.0.2)
12
37
  i18n (>= 0.7, < 2)
@@ -16,23 +41,58 @@ GEM
16
41
  bundler
17
42
  rake
18
43
  thor (>= 0.14.0)
44
+ arel (9.0.0)
45
+ benchmark-perf (0.2.1)
46
+ builder (3.2.3)
47
+ byebug (10.0.2)
48
+ case_transform (0.2)
49
+ activesupport
19
50
  coderay (1.1.2)
20
51
  concurrent-ruby (1.0.5)
52
+ crass (1.0.4)
21
53
  diff-lcs (1.3)
54
+ erubi (1.7.1)
22
55
  fast_jsonapi (1.2)
23
56
  activesupport (>= 4.2)
24
57
  i18n (1.0.1)
25
58
  concurrent-ruby (~> 1.0)
59
+ jsonapi-deserializable (0.2.0)
60
+ jsonapi-rb (0.5.0)
61
+ jsonapi-deserializable (~> 0.2.0)
62
+ jsonapi-serializable (~> 0.3.0)
63
+ jsonapi-renderer (0.2.0)
64
+ jsonapi-serializable (0.3.0)
65
+ jsonapi-renderer (~> 0.2.0)
66
+ jsonapi-serializers (1.0.1)
67
+ activesupport
68
+ loofah (2.2.2)
69
+ crass (~> 1.0.2)
70
+ nokogiri (>= 1.5.9)
26
71
  method_source (0.9.0)
72
+ mini_portile2 (2.3.0)
27
73
  minitest (5.11.3)
74
+ nokogiri (1.8.4)
75
+ mini_portile2 (~> 2.3.0)
76
+ oj (3.6.3)
28
77
  pry (0.11.3)
29
78
  coderay (~> 1.1.0)
30
79
  method_source (~> 0.9.0)
80
+ rack (2.0.5)
81
+ rack-test (1.0.0)
82
+ rack (>= 1.0, < 3)
83
+ rails-dom-testing (2.0.3)
84
+ activesupport (>= 4.2.0)
85
+ nokogiri (>= 1.6)
86
+ rails-html-sanitizer (1.0.4)
87
+ loofah (~> 2.2, >= 2.2.2)
31
88
  rake (10.5.0)
32
89
  rspec (3.7.0)
33
90
  rspec-core (~> 3.7.0)
34
91
  rspec-expectations (~> 3.7.0)
35
92
  rspec-mocks (~> 3.7.0)
93
+ rspec-benchmark (0.3.0)
94
+ benchmark-perf (~> 0.2.0)
95
+ rspec (>= 3.0.0, < 4.0.0)
36
96
  rspec-core (3.7.1)
37
97
  rspec-support (~> 3.7.0)
38
98
  rspec-expectations (3.7.0)
@@ -42,6 +102,7 @@ GEM
42
102
  diff-lcs (>= 1.2.0, < 2.0)
43
103
  rspec-support (~> 3.7.0)
44
104
  rspec-support (3.7.1)
105
+ sqlite3 (1.3.13)
45
106
  thor (0.20.0)
46
107
  thread_safe (0.3.6)
47
108
  tzinfo (1.2.5)
@@ -51,12 +112,20 @@ PLATFORMS
51
112
  ruby
52
113
 
53
114
  DEPENDENCIES
115
+ active_model_serializers (~> 0.10.7)
116
+ activerecord (>= 4.2)
54
117
  appraisal
55
118
  bundler (~> 1.16)
119
+ byebug
120
+ jsonapi-rb (~> 0.5.0)
121
+ jsonapi-serializers (~> 1.0.0)
56
122
  lp-serializable!
123
+ oj (~> 3.3)
57
124
  pry
58
125
  rake (~> 10.0)
59
126
  rspec (~> 3.0)
127
+ rspec-benchmark (~> 0.3.0)
128
+ sqlite3 (~> 1.3)
60
129
 
61
130
  BUNDLED WITH
62
131
  1.16.1
data/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # Lp::Serializable
2
2
 
3
- Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/lp/serializable`. To experiment with that code, run `bin/console` for an interactive prompt.
3
+ When serializing with [fast_jsonapi](https://github.com/Netflix/fast_jsonapi), data is structured per the json-api [specs](http://jsonapi.org/format/).
4
4
 
5
- TODO: Delete this and the text above, and describe your gem
5
+ lp-serializable is a thin wrapper around fast_jsonapi serialization, producting AMS style output.
6
+
7
+ lp-serializable is intended to be used in Rails controllers.
6
8
 
7
9
  ## Installation
8
10
 
@@ -22,7 +24,107 @@ Or install it yourself as:
22
24
 
23
25
  ## Usage
24
26
 
25
- TODO: Write usage instructions here
27
+ ### Controller Definition
28
+
29
+ ```ruby
30
+ class ApplicationController < ActionController::Base
31
+ include Lp::Serializable
32
+ end
33
+
34
+ class MoviesController < ApplicationController
35
+ def index
36
+ movies = Movie.all
37
+ movies_hash = serialize_and_flatten_collection(movies, 'Movie')
38
+ render json: movies_hash
39
+ end
40
+
41
+ def show
42
+ movie = Movie.find(params[:id])
43
+ movie_hash = serialize_and_flatten(movie)
44
+ render json: movie
45
+ end
46
+ end
47
+ ```
48
+
49
+ ### Serializer Definition
50
+
51
+ ```ruby
52
+ class MovieSerializer
53
+ include FastJsonapi::ObjectSerializer
54
+
55
+ attributes :name, :year
56
+
57
+ has_many :actors
58
+ belongs_to :owner
59
+ end
60
+
61
+ class ActorSerializer
62
+ include FastJsonapi::ObjectSerializer
63
+
64
+ attributes :id
65
+ end
66
+
67
+ class OwnerSerializer
68
+ include FastJsonapi::ObjectSerializer
69
+
70
+ attributes :id
71
+ end
72
+ ```
73
+
74
+ ### Sample Object
75
+
76
+ ```ruby
77
+ movie = Movie.new
78
+ movie.id = 232
79
+ movie.name = 'test movie'
80
+ movie.actor_ids = [1, 2, 3]
81
+ movie.owner_id = 3
82
+ movie.movie_type_id = 1
83
+ movie
84
+ ```
85
+
86
+ ### Object Serialization
87
+
88
+ #### Return a hash
89
+ ```ruby
90
+ hash = serialize_and_flatten(movie)
91
+ ```
92
+
93
+ #### Output
94
+
95
+ ```json
96
+ {
97
+ "data": {
98
+ "id": "3",
99
+ "type": "movie",
100
+ "name": "test movie",
101
+ "year": null,
102
+ "actors": [
103
+ {
104
+ "id": "1",
105
+ "type": "actor"
106
+ },
107
+ {
108
+ "id": "2",
109
+ "type": "actor"
110
+ }
111
+ ],
112
+ "owner": {
113
+ "id": "3",
114
+ "type": "user"
115
+ }
116
+ }
117
+ }
118
+
119
+ ```
120
+
121
+ For more information on configuration, refer to [fast_jsonapi](https://github.com/Netflix/fast_jsonapi#customizable-options) documentation.
122
+
123
+ ## Options Support
124
+
125
+ lp-serializable does not currently support fast_jsonapi's options hash when instantiating serializers.
126
+
127
+ This affects serializing with [params](https://github.com/Netflix/fast_jsonapi#params), [compound documents](https://github.com/Netflix/fast_jsonapi#compound-document), metadata, and links.
26
128
 
27
129
  ## Development
28
130
 
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'logger'
4
+
5
+ # Usage:
6
+ # class Movie
7
+ # def to_json(payload)
8
+ # FastJsonapi::MultiToJson.to_json(payload)
9
+ # end
10
+ # end
11
+ module FastJsonapi
12
+ module MultiToJson
13
+ # Result object pattern is from https://johnnunemaker.com/resilience-in-ruby/
14
+ # e.g. https://github.com/github/github-ds/blob/fbda5389711edfb4c10b6c6bad19311dfcb1bac1/lib/github/result.rb
15
+ class Result
16
+ def initialize(*rescued_exceptions)
17
+ @rescued_exceptions = if rescued_exceptions.empty?
18
+ [StandardError]
19
+ else
20
+ rescued_exceptions
21
+ end
22
+
23
+ @value = yield
24
+ @error = nil
25
+ rescue *rescued_exceptions => e
26
+ @error = e
27
+ end
28
+
29
+ def ok?
30
+ @error.nil?
31
+ end
32
+
33
+ def value!
34
+ if ok?
35
+ @value
36
+ else
37
+ raise @error
38
+ end
39
+ end
40
+
41
+ def rescue
42
+ return self if ok?
43
+
44
+ Result.new(*@rescued_exceptions) { yield(@error) }
45
+ end
46
+ end
47
+
48
+ def self.logger(device=nil)
49
+ return @logger = Logger.new(device) if device
50
+ @logger ||= Logger.new(IO::NULL)
51
+ end
52
+
53
+ # Encoder-compatible with default MultiJSON adapters and defaults
54
+ def self.to_json_method
55
+ encode_method = String.new(%(def _fast_to_json(object)\n ))
56
+ encode_method << Result.new(LoadError) {
57
+ require 'oj'
58
+ %(::Oj.dump(object, mode: :compat, time_format: :ruby, use_to_json: true))
59
+ }.rescue {
60
+ require 'yajl'
61
+ %(::Yajl::Encoder.encode(object))
62
+ }.rescue {
63
+ require 'jrjackson' unless defined?(::JrJackson)
64
+ %(::JrJackson::Json.dump(object))
65
+ }.rescue {
66
+ require 'json'
67
+ %(JSON.fast_generate(object, create_additions: false, quirks_mode: true))
68
+ }.rescue {
69
+ require 'gson'
70
+ %(::Gson::Encoder.new({}).encode(object))
71
+ }.rescue {
72
+ require 'active_support/json/encoding'
73
+ %(::ActiveSupport::JSON.encode(object))
74
+ }.rescue {
75
+ warn "No JSON encoder found. Falling back to `object.to_json`"
76
+ %(object.to_json)
77
+ }.value!
78
+ encode_method << "\nend"
79
+ end
80
+
81
+ def self.to_json(object)
82
+ _fast_to_json(object)
83
+ rescue NameError
84
+ define_to_json(FastJsonapi::MultiToJson)
85
+ _fast_to_json(object)
86
+ end
87
+
88
+ def self.define_to_json(receiver)
89
+ cl = caller_locations[0]
90
+ method_body = to_json_method
91
+ logger.debug { "Defining #{receiver}._fast_to_json as #{method_body.inspect}" }
92
+ receiver.instance_eval method_body, cl.absolute_path, cl.lineno
93
+ end
94
+
95
+ def self.reset_to_json!
96
+ undef :_fast_to_json if method_defined?(:_fast_to_json)
97
+ logger.debug { "Undefining #{receiver}._fast_to_json" }
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/core_ext/object'
4
+ require 'active_support/concern'
5
+ require 'active_support/inflector'
6
+ require 'fast_jsonapi/serialization_core'
7
+
8
+ module FastJsonapi
9
+ module ObjectSerializer
10
+ extend ActiveSupport::Concern
11
+ include SerializationCore
12
+
13
+ SERIALIZABLE_HASH_NOTIFICATION = 'render.fast_jsonapi.serializable_hash'
14
+ SERIALIZED_JSON_NOTIFICATION = 'render.fast_jsonapi.serialized_json'
15
+
16
+ included do
17
+ # Set record_type based on the name of the serializer class
18
+ set_type(reflected_record_type) if reflected_record_type
19
+ end
20
+
21
+ def initialize(resource, options = {})
22
+ process_options(options)
23
+
24
+ @resource = resource
25
+ end
26
+
27
+ def serializable_hash
28
+ return hash_for_collection if is_collection?(@resource)
29
+
30
+ hash_for_one_record
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
+ serializable_hash[:links] = @links if @links.present?
38
+
39
+ return serializable_hash unless @resource
40
+
41
+ serializable_hash[:data] = self.class.record_hash(@resource, @params)
42
+ serializable_hash[:included] = self.class.get_included_records(@resource, @includes, @known_included_objects, @params) if @includes.present?
43
+ serializable_hash
44
+ end
45
+
46
+ def hash_for_collection
47
+ serializable_hash = {}
48
+
49
+ data = []
50
+ included = []
51
+ @resource.each do |record|
52
+ data << self.class.record_hash(record, @params)
53
+ included.concat self.class.get_included_records(record, @includes, @known_included_objects, @params) if @includes.present?
54
+ end
55
+
56
+ serializable_hash[:data] = data
57
+ serializable_hash[:included] = included if @includes.present?
58
+ serializable_hash[:meta] = @meta if @meta.present?
59
+ serializable_hash[:links] = @links if @links.present?
60
+ serializable_hash
61
+ end
62
+
63
+ def serialized_json
64
+ self.class.to_json(serializable_hash)
65
+ end
66
+
67
+ private
68
+
69
+ def process_options(options)
70
+ return if options.blank?
71
+
72
+ @known_included_objects = {}
73
+ @meta = options[:meta]
74
+ @links = options[:links]
75
+ @params = options[:params] || {}
76
+ raise ArgumentError.new("`params` option passed to serializer must be a hash") unless @params.is_a?(Hash)
77
+
78
+ if options[:include].present?
79
+ @includes = options[:include].delete_if(&:blank?).map(&:to_sym)
80
+ self.class.validate_includes!(@includes)
81
+ end
82
+ end
83
+
84
+ def is_collection?(resource)
85
+ resource.respond_to?(:each) && !resource.respond_to?(:each_pair)
86
+ end
87
+
88
+ class_methods do
89
+
90
+ def inherited(subclass)
91
+ super(subclass)
92
+ subclass.attributes_to_serialize = attributes_to_serialize.dup if attributes_to_serialize.present?
93
+ subclass.relationships_to_serialize = relationships_to_serialize.dup if relationships_to_serialize.present?
94
+ subclass.cachable_relationships_to_serialize = cachable_relationships_to_serialize.dup if cachable_relationships_to_serialize.present?
95
+ subclass.uncachable_relationships_to_serialize = uncachable_relationships_to_serialize.dup if uncachable_relationships_to_serialize.present?
96
+ subclass.transform_method = transform_method
97
+ subclass.cache_length = cache_length
98
+ subclass.race_condition_ttl = race_condition_ttl
99
+ subclass.data_links = data_links
100
+ subclass.cached = cached
101
+ end
102
+
103
+ def reflected_record_type
104
+ return @reflected_record_type if defined?(@reflected_record_type)
105
+
106
+ @reflected_record_type ||= begin
107
+ if self.name.end_with?('Serializer')
108
+ self.name.split('::').last.chomp('Serializer').underscore.to_sym
109
+ end
110
+ end
111
+ end
112
+
113
+ def set_key_transform(transform_name)
114
+ mapping = {
115
+ camel: :camelize,
116
+ camel_lower: [:camelize, :lower],
117
+ dash: :dasherize,
118
+ underscore: :underscore
119
+ }
120
+ self.transform_method = mapping[transform_name.to_sym]
121
+ end
122
+
123
+ def run_key_transform(input)
124
+ if self.transform_method.present?
125
+ input.to_s.send(*@transform_method).to_sym
126
+ else
127
+ input.to_sym
128
+ end
129
+ end
130
+
131
+ def use_hyphen
132
+ warn('DEPRECATION WARNING: use_hyphen is deprecated and will be removed from fast_jsonapi 2.0 use (set_key_transform :dash) instead')
133
+ set_key_transform :dash
134
+ end
135
+
136
+ def set_type(type_name)
137
+ self.record_type = run_key_transform(type_name)
138
+ end
139
+
140
+ def set_id(id_name)
141
+ self.record_id = id_name
142
+ end
143
+
144
+ def cache_options(cache_options)
145
+ self.cached = cache_options[:enabled] || false
146
+ self.cache_length = cache_options[:cache_length] || 5.minutes
147
+ self.race_condition_ttl = cache_options[:race_condition_ttl] || 5.seconds
148
+ end
149
+
150
+ def attributes(*attributes_list, &block)
151
+ attributes_list = attributes_list.first if attributes_list.first.class.is_a?(Array)
152
+ self.attributes_to_serialize = {} if self.attributes_to_serialize.nil?
153
+ attributes_list.each do |attr_name|
154
+ method_name = attr_name
155
+ key = run_key_transform(method_name)
156
+ attributes_to_serialize[key] = block || method_name
157
+ end
158
+ end
159
+
160
+ alias_method :attribute, :attributes
161
+
162
+ def add_relationship(name, relationship)
163
+ self.relationships_to_serialize = {} if relationships_to_serialize.nil?
164
+ self.cachable_relationships_to_serialize = {} if cachable_relationships_to_serialize.nil?
165
+ self.uncachable_relationships_to_serialize = {} if uncachable_relationships_to_serialize.nil?
166
+
167
+ if !relationship[:cached]
168
+ self.uncachable_relationships_to_serialize[name] = relationship
169
+ else
170
+ self.cachable_relationships_to_serialize[name] = relationship
171
+ end
172
+ self.relationships_to_serialize[name] = relationship
173
+ end
174
+
175
+ def has_many(relationship_name, options = {}, &block)
176
+ name = relationship_name.to_sym
177
+ hash = create_relationship_hash(relationship_name, :has_many, options, block)
178
+ add_relationship(name, hash)
179
+ end
180
+
181
+ def has_one(relationship_name, options = {}, &block)
182
+ name = relationship_name.to_sym
183
+ hash = create_relationship_hash(relationship_name, :has_one, options, block)
184
+ add_relationship(name, hash)
185
+ end
186
+
187
+ def belongs_to(relationship_name, options = {}, &block)
188
+ name = relationship_name.to_sym
189
+ hash = create_relationship_hash(relationship_name, :belongs_to, options, block)
190
+ add_relationship(name, hash)
191
+ end
192
+
193
+ def create_relationship_hash(base_key, relationship_type, options, block)
194
+ name = base_key.to_sym
195
+ if relationship_type == :has_many
196
+ base_serialization_key = base_key.to_s.singularize
197
+ base_key_sym = base_serialization_key.to_sym
198
+ id_postfix = '_ids'
199
+ else
200
+ base_serialization_key = base_key
201
+ base_key_sym = name
202
+ id_postfix = '_id'
203
+ end
204
+ {
205
+ key: options[:key] || run_key_transform(base_key),
206
+ name: name,
207
+ id_method_name: options[:id_method_name] || "#{base_serialization_key}#{id_postfix}".to_sym,
208
+ record_type: options[:record_type] || run_key_transform(base_key_sym),
209
+ object_method_name: options[:object_method_name] || name,
210
+ object_block: block,
211
+ serializer: compute_serializer_name(options[:serializer] || base_key_sym),
212
+ relationship_type: relationship_type,
213
+ cached: options[:cached] || false,
214
+ polymorphic: fetch_polymorphic_option(options)
215
+ }
216
+ end
217
+
218
+ def compute_serializer_name(serializer_key)
219
+ return serializer_key unless serializer_key.is_a? Symbol
220
+ namespace = self.name.gsub(/()?\w+Serializer$/, '')
221
+ serializer_name = serializer_key.to_s.classify + 'Serializer'
222
+ (namespace + serializer_name).to_sym
223
+ end
224
+
225
+ def fetch_polymorphic_option(options)
226
+ option = options[:polymorphic]
227
+ return false unless option.present?
228
+ return option if option.respond_to? :keys
229
+ {}
230
+ end
231
+
232
+ def link(link_name, link_method_name = nil, &block)
233
+ self.data_links = {} if self.data_links.nil?
234
+ link_method_name = link_name if link_method_name.nil?
235
+ key = run_key_transform(link_name)
236
+ self.data_links[key] = block || link_method_name
237
+ end
238
+
239
+ def validate_includes!(includes)
240
+ return if includes.blank?
241
+
242
+ includes.detect do |include_item|
243
+ klass = self
244
+ parse_include_item(include_item).each do |parsed_include|
245
+ relationship_to_include = klass.relationships_to_serialize[parsed_include]
246
+ raise ArgumentError, "#{parsed_include} is not specified as a relationship on #{klass.name}" unless relationship_to_include
247
+ raise NotImplementedError if relationship_to_include[:polymorphic].is_a?(Hash)
248
+ klass = relationship_to_include[:serializer].to_s.constantize
249
+ end
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end
@@ -0,0 +1,192 @@
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_to_serialize.each_with_object({}) do |(key, method), attr_hash|
77
+ attr_hash[key] = if method.is_a?(Proc)
78
+ method.arity == 1 ? method.call(record) : method.call(record, params)
79
+ else
80
+ record.public_send(method)
81
+ end
82
+ end
83
+ end
84
+
85
+ def relationships_hash(record, relationships = nil, params = {})
86
+ relationships = relationships_to_serialize if relationships.nil?
87
+
88
+ relationships.each_with_object({}) do |(_k, relationship), hash|
89
+ name = relationship[:key]
90
+ empty_case = relationship[:relationship_type] == :has_many ? [] : nil
91
+ hash[name] = {
92
+ data: ids_hash_from_record_and_relationship(record, relationship, params) || empty_case
93
+ }
94
+ end
95
+ end
96
+
97
+ def record_hash(record, params = {})
98
+ if cached
99
+ record_hash = Rails.cache.fetch(record.cache_key, expires_in: cache_length, race_condition_ttl: race_condition_ttl) do
100
+ temp_hash = id_hash(id_from_record(record), record_type, true)
101
+ temp_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present?
102
+ temp_hash[:relationships] = {}
103
+ temp_hash[:relationships] = relationships_hash(record, cachable_relationships_to_serialize, params) if cachable_relationships_to_serialize.present?
104
+ temp_hash[:links] = links_hash(record, params) if data_links.present?
105
+ temp_hash
106
+ end
107
+ record_hash[:relationships] = record_hash[:relationships].merge(relationships_hash(record, uncachable_relationships_to_serialize, params)) if uncachable_relationships_to_serialize.present?
108
+ record_hash
109
+ else
110
+ record_hash = id_hash(id_from_record(record), record_type, true)
111
+ record_hash[:attributes] = attributes_hash(record, params) if attributes_to_serialize.present?
112
+ record_hash[:relationships] = relationships_hash(record, nil, params) if relationships_to_serialize.present?
113
+ record_hash[:links] = links_hash(record, params) if data_links.present?
114
+ record_hash
115
+ end
116
+ end
117
+
118
+ def id_from_record(record)
119
+ return record.send(record_id) if record_id
120
+ raise MandatoryField, 'id is a mandatory field in the jsonapi spec' unless record.respond_to?(:id)
121
+ record.id
122
+ end
123
+
124
+ # Override #to_json for alternative implementation
125
+ def to_json(payload)
126
+ FastJsonapi::MultiToJson.to_json(payload) if payload.present?
127
+ end
128
+
129
+ def parse_include_item(include_item)
130
+ return [include_item.to_sym] unless include_item.to_s.include?('.')
131
+ include_item.to_s.split('.').map { |item| item.to_sym }
132
+ end
133
+
134
+ def remaining_items(items)
135
+ return unless items.size > 1
136
+
137
+ items_copy = items.dup
138
+ items_copy.delete_at(0)
139
+ [items_copy.join('.').to_sym]
140
+ end
141
+
142
+ # includes handler
143
+ def get_included_records(record, includes_list, known_included_objects, params = {})
144
+ return unless includes_list.present?
145
+
146
+ includes_list.sort.each_with_object([]) do |include_item, included_records|
147
+ items = parse_include_item(include_item)
148
+ items.each do |item|
149
+ next unless relationships_to_serialize && relationships_to_serialize[item]
150
+ raise NotImplementedError if @relationships_to_serialize[item][:polymorphic].is_a?(Hash)
151
+ record_type = @relationships_to_serialize[item][:record_type]
152
+ serializer = @relationships_to_serialize[item][:serializer].to_s.constantize
153
+ relationship_type = @relationships_to_serialize[item][:relationship_type]
154
+
155
+ included_objects = fetch_associated_object(record, @relationships_to_serialize[item], params)
156
+ next if included_objects.blank?
157
+ included_objects = [included_objects] unless relationship_type == :has_many
158
+
159
+ included_objects.each do |inc_obj|
160
+ if remaining_items(items)
161
+ serializer_records = serializer.get_included_records(inc_obj, remaining_items(items), known_included_objects)
162
+ included_records.concat(serializer_records) unless serializer_records.empty?
163
+ end
164
+
165
+ code = "#{record_type}_#{inc_obj.id}"
166
+ next if known_included_objects.key?(code)
167
+
168
+ known_included_objects[code] = inc_obj
169
+ included_records << serializer.record_hash(inc_obj, params)
170
+ end
171
+ end
172
+ end
173
+ end
174
+
175
+ def fetch_associated_object(record, relationship, params)
176
+ return relationship[:object_block].call(record, params) unless relationship[:object_block].nil?
177
+ record.send(relationship[:object_method_name])
178
+ end
179
+
180
+ def fetch_id(record, relationship, params)
181
+ unless relationship[:object_block].nil?
182
+ object = relationship[:object_block].call(record, params)
183
+
184
+ return object.map(&:id) if object.respond_to? :map
185
+ return object.id
186
+ end
187
+
188
+ record.public_send(relationship[:id_method_name])
189
+ end
190
+ end
191
+ end
192
+ end
@@ -3,6 +3,8 @@ module Lp
3
3
  module Utilities
4
4
  private
5
5
 
6
+ REDUNDANT_KEYS = %i(attributes relationships)
7
+
6
8
  def nest_data?(resource, nested)
7
9
  if nested
8
10
  resource
@@ -34,15 +36,26 @@ module Lp
34
36
  def flatten_hash(hash)
35
37
  return unless hash
36
38
  hash.each_with_object({}) do |(k, v), h|
37
- if v.is_a?(Hash) && k == :attributes
39
+ if hash_and_matches_redundant_keys?(v, k)
38
40
  flatten_hash(v).map do |h_k, h_v|
39
- h["#{h_k}".to_sym] = h_v
41
+ h[h_k.to_s.to_sym] = h_v
40
42
  end
41
- else
43
+ # NOTE: extract this into a different method?
44
+ elsif hash_and_has_data_key?(v)
45
+ h[k] = expose_data(v)
46
+ else
42
47
  h[k] = v
43
48
  end
44
49
  end
45
50
  end
51
+
52
+ def hash_and_has_data_key?(value)
53
+ value.is_a?(Hash) && value.key?(:data)
54
+ end
55
+
56
+ def hash_and_matches_redundant_keys?(value, key)
57
+ value.is_a?(Hash) && REDUNDANT_KEYS.any?{|sym| sym == key}
58
+ end
46
59
  end
47
60
  end
48
61
  end
@@ -1,5 +1,5 @@
1
1
  module Lp
2
2
  module Serializable
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
5
5
  end
@@ -1,4 +1,3 @@
1
- require "lp/serializable/version"
2
1
  require "lp/serializable/strategies"
3
2
 
4
3
  module Lp
@@ -19,13 +18,15 @@ module Lp
19
18
  def serialize_and_flatten_with_class_name(resource, class_name, options={})
20
19
  raise UnserializableCollection if resource.is_a?(Array)
21
20
  base_hash = serializable_hash_with_class_name(resource,
22
- class_name, options)
21
+ class_name,
22
+ options)
23
23
  flatten_and_nest_data(base_hash, set_nested_option(options))
24
24
  end
25
25
 
26
26
  def serialize_and_flatten_collection(resource, class_name, options={})
27
27
  base_hash = serializable_hash_with_class_name(resource,
28
- class_name, options)
28
+ class_name,
29
+ options)
29
30
  flatten_array_and_nest_data(base_hash, set_nested_option(options))
30
31
  end
31
32
  end
@@ -10,7 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.email = ["nate@mrjones.io"]
11
11
 
12
12
  spec.summary = "Serialize with Fast JSON API, flatten like AWS"
13
- # spec.description = %q{TODO: Write a longer description or delete this line.}
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"
40
- end
48
+ spec.add_dependency "fast_jsonapi", '>= 1.2'
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: 0.2.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: 2018-07-14 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.2'
216
+ type: :runtime
217
+ prerelease: false
218
+ version_requirements: !ruby/object:Gem::Requirement
219
+ requirements:
220
+ - - ">="
221
+ - !ruby/object:Gem::Version
222
+ version: '1.2'
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,7 @@ extra_rdoc_files: []
103
230
  files:
104
231
  - ".gitignore"
105
232
  - ".rspec"
233
+ - ".ruby-version"
106
234
  - ".travis.yml"
107
235
  - CODE_OF_CONDUCT.md
108
236
  - Gemfile
@@ -112,6 +240,9 @@ files:
112
240
  - Rakefile
113
241
  - bin/console
114
242
  - bin/setup
243
+ - lib/fast_jsonapi/multi_to_json.rb
244
+ - lib/fast_jsonapi/object_serializer.rb
245
+ - lib/fast_jsonapi/serialization_core.rb
115
246
  - lib/lp/serializable.rb
116
247
  - lib/lp/serializable/strategies.rb
117
248
  - lib/lp/serializable/utilities.rb