lp-serializable 0.1.0 → 0.2.0

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