jsonapi_serializer 0.1.0 → 0.1.1

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
- SHA1:
3
- metadata.gz: 21d607d922e598bf9866f8b829def185ff0f1f72
4
- data.tar.gz: 5a80e053e723f7180018d582d2871426f713723f
2
+ SHA256:
3
+ metadata.gz: 54a1f6c12504a34ad1658cc5324b324d374e510018db28b152bf9f043d26fc3e
4
+ data.tar.gz: 201ed0fc8df08e5c4abc74fcd18493f9e5711d0612f397c9d369b11b8fd08c58
5
5
  SHA512:
6
- metadata.gz: 1c727a0615f60c522a665e6725e91b8a5770d8c959cf60f4ebb176da8db2d5322e2bb8faeb6eb239e106e3a9d71a4b7bcfcae2f4163b5c72c7274dab0cdd0820
7
- data.tar.gz: 88855eeaf4da64bd790379072c66881512ca675c5d0137600314200df71795ce94cbb58072a83363407557ce533ab416964ceea647855ef3207e89813c66b805
6
+ metadata.gz: fd85b7ace1691b5916781df14b6074d77502b3eb8b387b86875f4bdf587d39e3ccd3eba6d9ce0d218489e5f0f94d7ff8911beada569cf4f5dbd15a99fe3e668c
7
+ data.tar.gz: fdc211f59a28ded7c07cbf56109673047285e09a3650d62d66f09fed509e8a3b996e944f47f6278bd67db67d99db0fbe59bb6d1a8a6c5e672bb434ae31f9b663
@@ -0,0 +1,9 @@
1
+ ### 0.1.1
2
+
3
+ - Support for lambda as a source of relationships (`from` parameter)
4
+ - Instance level attributes for fields and includes are removed in favor of initialization-time variables distribution (No public API changed)
5
+ - Fixed bug when `key_transform` didn't have an effect on serialized attributes and relationships.
6
+
7
+ ### 0.1.0
8
+
9
+ Initial release
data/README.md CHANGED
@@ -82,8 +82,8 @@ JsonapiSerializer.set_type_namespace_separator :ignore
82
82
 
83
83
  # The default option is "_"
84
84
  # Bear in mind that only " " (space), "_" and "-" or any combination of these
85
- # are allowed as per json-api spec and attempt to set anything else
86
- # will cause an error.
85
+ # are allowed as per json-api spec, but in practice you can use other symbols if you
86
+ # make sure to escape them while using in urls.
87
87
  ```
88
88
 
89
89
  ### Attributes configuration
@@ -118,13 +118,17 @@ In this example, serializer will access `record.name` and `record.year` to fill
118
118
 
119
119
  ### Relationships configuration
120
120
 
121
- In order to define relationships, you can use `belongs_to` and `has_many` DSL methods. They accept options `serializer` and `from`. By default, serializer will try to guess relationship serializer by the name of relation. In case of `:director` relation, it would try to use `DirectorSerializer` and crash since it's not defined. Use `serializer` parameter to set serializer explicitly. Option `from` is used to point at the attribute of the model that actually returns the relation object(s) if it's different.
121
+ In order to define relationships, you can use `belongs_to` and `has_many` DSL methods. They accept options `serializer` and `from`. By default, serializer will try to guess relationship serializer by the name of relation. In case of `:director` relation, it would try to use `DirectorSerializer` and crash since it's not defined. Use `serializer` parameter to set serializer explicitly. Option `from` is used to point at the attribute of the model that actually returns the relation object(s) if it's different. You can also supply lambda, if the relation is accessible in some non-trivial way.
122
122
 
123
123
  ```ruby
124
124
  class MovieSerializer
125
125
  include JsonapiSerializer::Base
126
126
  belongs_to :director, serializer: PersonSerializer
127
127
  has_many :actors, from: :cast
128
+
129
+ # or if you want to introduce some logic in building relationships
130
+ has_many :male_actors, from: lambda { |movie| movie.cast.where(sex: "m") }
131
+ has_many :female_actors, from: lambda { |movie| movie.cast.where(sex: "f") }
128
132
  end
129
133
  ```
130
134
 
@@ -137,7 +141,7 @@ From the perspective of serializer, there is no distinction between `belongs_to`
137
141
 
138
142
  There are two kinds of polymorphism that `jsonapi_serializer` supports. First is polymorphic model (STI models in ActiveRecord), where most attributes are shared, but children have different types. Ultimately it is still one kind of entity: think of `Vehicle` base class inherited by `Car`, `Truck` and `Motorcycle`. Second kind is polymorphic relationship, where one relationship can contain entirely different models. Let's say you have `Post` and `Product`, and both can have comments, hence from the perspective of individual comment it belongs to `Commentable`. Even though `Post` and `Model` can share some attributes, their serializers will be used mostly along from comments.
139
143
 
140
- These types of serializers share most of the implementation implemented similarly, they share most of the logic and both need a `resolver`, which is implicitly defined as a lambda, that applies `JsonapiSerializer.type_transform` to the record class name.
144
+ These types of serializers share most of the implementation and both rely on `resolver`, which is implicitly defined as a lambda, that applies `JsonapiSerializer.type_transform` to the record class name.
141
145
 
142
146
  #### Polymorphic Models
143
147
 
@@ -209,6 +213,8 @@ Once serializers are defined, you can instantiate them with several options. Cur
209
213
 
210
214
  `fields` must be a hash, where keys represent record types and values are list of attributes and relationships of the corresponding type that will be present in serialized object. If some type is missing, that means all attributes and relationships defined in serializer will be serialized. In case of `polymorphic` serializer, you can supply shared fields under polymorphic type. **_There is a caveat, though: if you define a fieldset for a parent polymorphic class and omit fieldsets for subclasses it will be considered that you did not want any of attributes and relationships defined in subclass to be serialized._** It works the same fashion for polymorphic relationships, so if you want only `title` from `Post` and `name` from `Product`, you can supply `{commentable: ["title", "name"]}` as a `fields` parameter for `CommentableSerializer`.
211
215
 
216
+ `fields` must have attributes as seen by API consumer. For example, if you have `key_transform` set to `:camelize`, then fields will be expected as `{"movie" => ["movieTitle", "releaseYear"]}`, you can use symbols or strings, they will be normallized on serializer instantiation.
217
+
212
218
  `include` defines an arbitrary depth tree of included relationships in a similar way as ActiveRecord's `includes`. Bear in mind that `fields` has precedence, which means that if some relationship is missing in fields, it will not be included either.
213
219
 
214
220
  ```ruby
@@ -236,23 +242,42 @@ serializer.serialazable_hash(movies, meta: meta)
236
242
  serializer.serialized_json(movies, meta: meta)
237
243
  ```
238
244
 
245
+ ### Utils
246
+
247
+ `JsonapiSerializer` provides some convenience methods for converting `fields` and `include` from query parameters into the form accepted by the serializer.
248
+
249
+ #### Fields converter
250
+
251
+ ```ruby
252
+ JsonapiSerializer.convert_fields({"articles" => "title,body", "people" => "name"})
253
+ # {articles: [:title, :body], people: [:name]}
254
+ ```
255
+
256
+ #### Include converter
257
+
258
+ ```ruby
259
+ JsonapiSerializer.convert_include("author,comments.author,comments.theme")
260
+ # {author: {}, comments: {author: {}, theme: {}}}
261
+ end
262
+ ```
263
+
239
264
  ## Performance
240
265
 
241
266
  By running `bin/benchmark` you can launch performance test locally, however numbers are fluctuating widely. The example output is as follows:
242
267
 
243
268
  ### Base case
244
269
 
245
- | Adapters | 10 hash/json (ms) | 100 hash/json (ms) | 1000 hash/json (ms) | 10000 hash/json (ms) |
246
- | -------------------- |:--------------------:|:--------------------:|:--------------------:|:--------------------:|
247
- | JsonapiSerializerTest | 0.36 / 1.17 | 1.55 / 1.98 | 13.74 / 20.18 | 156.31 / 208.86 |
248
- | FastJsonapiTest | 0.16 / 0.19 | 1.14 / 1.75 | 11.86 / 18.13 | 124.13 / 176.97 |
270
+ | Adapters | 10 hash/json (ms) | 100 hash/json (ms) | 1000 hash/json (ms) | 10000 hash/json (ms) |
271
+ | ------------------------ |:------------------------:|:------------------------:|:------------------------:|:------------------------:|
272
+ | JsonapiSerializerTest | 0.39 / 1.17 | 1.32 / 1.75 | 11.26 / 16.55 | 118.13 / 179.28 |
273
+ | FastJsonapiTest | 0.16 / 0.19 | 1.12 / 1.60 | 10.71 / 16.02 | 104.76 / 160.39 |
249
274
 
250
275
  ### With includes
251
276
 
252
- | Adapters | 10 hash/json (ms) | 100 hash/json (ms) | 1000 hash/json (ms) | 10000 hash/json (ms) |
253
- | -------------------- |:--------------------:|:--------------------:|:--------------------:|:--------------------:|
254
- | JsonapiSerializerTest | 0.51 / 0.46 | 2.05 / 2.50 | 15.28 / 21.59 | 159.89 / 214.49 |
255
- | FastJsonapiTest | 0.26 / 0.25 | 2.01 / 2.47 | 15.54 / 20.11 | 154.82 / 211.48 |
277
+ | Adapters | 10 hash/json (ms) | 100 hash/json (ms) | 1000 hash/json (ms) | 10000 hash/json (ms) |
278
+ | ------------------------ |:------------------------:|:------------------------:|:------------------------:|:------------------------:|
279
+ | JsonapiSerializerTest | 0.48 / 0.44 | 1.72 / 2.47 | 13.04 / 17.71 | 125.47 / 179.12 |
280
+ | FastJsonapiTest | 0.27 / 0.26 | 1.84 / 2.11 | 13.64 / 17.85 | 141.91 / 222.25 |
256
281
 
257
282
  Performance tests do not include any advanced features, such as fieldsets, nested includes or polymorphic serializers, and were mostly intended to make sure that adding these features did not make serializer slower (or at least significantly slower), but there are models prepared to extend these tests. PRs are welcome.
258
283
 
@@ -2,16 +2,21 @@
2
2
 
3
3
  require "bundler/setup"
4
4
  require "jsonapi_serializer"
5
+ require "ruby-prof"
5
6
  require "./perf/runner"
6
7
  require "./perf/jsonapi_serializer_test"
7
8
  require "./perf/fast_jsonapi_test"
8
9
  GC.disable
10
+ # RubyProf.measure_mode = RubyProf::WALL_TIME
9
11
 
10
12
  runner = Runner.new(10_000)
11
13
  runner.set_modules(JsonapiSerializerTest, FastJsonapiTest)
12
14
  runner.set_tests(:base, :with_included)
13
15
  runner.set_takes(10, 100, 1000, 10_000)
14
- runner.run
16
+
17
+ # result = RubyProf.profile do
18
+ runner.run
19
+ # end
15
20
 
16
21
  print "\n"
17
22
  print "### Base case\n"
@@ -21,3 +26,6 @@ print "\n"
21
26
  print "### With includes\n"
22
27
  print "\n"
23
28
  runner.print_table(:with_included)
29
+
30
+ # printer = RubyProf::GraphPrinter.new(result)
31
+ # printer.print(STDOUT, {})
@@ -26,5 +26,6 @@ Gem::Specification.new do |spec|
26
26
  spec.add_development_dependency "rake", "~> 10.0"
27
27
  spec.add_development_dependency "rspec", "~> 3.0"
28
28
  spec.add_development_dependency "ffaker", "~> 2.8.1"
29
+ spec.add_development_dependency "ruby-prof"
29
30
  spec.add_development_dependency "fast_jsonapi", "~> 1.0"
30
31
  end
@@ -15,12 +15,15 @@ module JsonapiSerializer::Base
15
15
  super(opts)
16
16
  @id = self.class.meta_id
17
17
  unless opts[:id_only]
18
- @attributes = []
19
- @relationships = []
20
- @includes = normalize_includes(opts.fetch(:include, []))
21
- prepare_fields(opts)
22
- prepare_attributes
23
- prepare_relationships
18
+ fields = normalize_fields(opts.fetch(:fields, {}))
19
+ if opts[:poly_fields].present?
20
+ fields[@type] = fields.fetch(@type, []) + opts[:poly_fields]
21
+ end
22
+
23
+ includes = normalize_includes(opts.fetch(:include, {}))
24
+
25
+ prepare_attributes(fields)
26
+ prepare_relationships(fields, includes)
24
27
  end
25
28
  end
26
29
 
@@ -35,19 +38,15 @@ module JsonapiSerializer::Base
35
38
  end
36
39
 
37
40
  def relationships_hash(record, context = {})
38
- @relationships.each_with_object({}) do |(key, type, from, serializer), hash|
39
- if rel = record.public_send(from)
40
- if rel.respond_to?(:each)
41
- hash[key] = {data: []}
42
- rel.each do |item|
43
- id = serializer.id_hash(item)
44
- hash[key][:data] << id
45
- add_included(serializer, item, id, context) if @includes.has_key?(key)
41
+ @relationships.each_with_object({}) do |(key, from, serializer, included), hash|
42
+ if relation = from.call(record)
43
+ if relation.respond_to?(:map)
44
+ relation_ids = relation.map do |item|
45
+ process_relation(item, serializer, context, included)
46
46
  end
47
+ hash[key] = {data: relation_ids}
47
48
  else
48
- id = serializer.id_hash(rel)
49
- hash[key] = {data: id}
50
- add_included(serializer, rel, id, context) if @includes.has_key?(key)
49
+ hash[key] = {data: process_relation(relation, serializer, context, included)}
51
50
  end
52
51
  else
53
52
  hash[key] = {data: nil}
@@ -66,34 +65,37 @@ module JsonapiSerializer::Base
66
65
  end
67
66
 
68
67
  private
69
- def prepare_fields(opts)
70
- @fields = opts.fetch(:fields, {})
71
- if opts[:poly_fields].present? || @fields[@type].present?
72
- @fields[@type] = opts.fetch(:poly_fields, []) + [*@fields.fetch(@type, [])].map { |f| JsonapiSerializer.key_transform(f) }
73
- @fields[@type].uniq!
74
- end
75
- end
76
-
77
- def prepare_attributes
78
- key_intersect(@fields[@type], self.class.meta_attributes.keys).each do |key|
79
- @attributes << [key, self.class.meta_attributes[key]]
68
+ def prepare_attributes(all_fields)
69
+ @attributes = []
70
+ fields = all_fields[@type]
71
+ self.class.meta_attributes.each do |attribute, getter|
72
+ key = JsonapiSerializer.key_transform(attribute)
73
+ if fields.nil? || fields.include?(key)
74
+ @attributes << [key, getter]
75
+ end
80
76
  end
81
77
  end
82
78
 
83
- def prepare_relationships
84
- key_intersect(@fields[@type], self.class.meta_relationships.keys).each do |key|
85
- rel = self.class.meta_relationships[key]
86
- serializer = rel[:serializer].new(
87
- fields: @fields,
88
- include: @includes.fetch(key, []),
89
- id_only: !@includes.has_key?(key)
90
- )
91
- @relationships << [key, rel[:type], rel[:from], serializer]
79
+ def prepare_relationships(all_fields, includes)
80
+ @relationships = []
81
+ relations = all_fields[@type]
82
+ self.class.meta_relationships.each do |relation, cfg|
83
+ key = JsonapiSerializer.key_transform(relation)
84
+ if relations.nil? || relations.include?(key)
85
+ included = includes.has_key?(relation)
86
+ serializer = cfg[:serializer].to_s.constantize.new(
87
+ fields: all_fields,
88
+ include: includes.fetch(relation, {}),
89
+ id_only: !included
90
+ )
91
+ @relationships << [relation, cfg[:from], serializer, included]
92
+ end
92
93
  end
93
94
  end
94
95
 
95
- def add_included(serializer, item, id, context)
96
- if (context[:tracker][id[:type]] ||= Set.new).add?(id[:id]).present?
96
+ def process_relation(item, serializer, context, included)
97
+ id = serializer.id_hash(item)
98
+ if included && (context[:tracker][id[:type]] ||= Set.new).add?(id[:id]).present?
97
99
  attributes = serializer.attributes_hash(item)
98
100
  relationships = serializer.relationships_hash(item, context)
99
101
  inc = id.clone
@@ -101,5 +103,6 @@ module JsonapiSerializer::Base
101
103
  inc[:relationships] = relationships if relationships.present?
102
104
  context[:included] << inc
103
105
  end
106
+ id
104
107
  end
105
108
  end
@@ -11,6 +11,9 @@ module JsonapiSerializer::DSL
11
11
  @meta_relationships = {}
12
12
 
13
13
  class << self
14
+ alias_method :has_many, :relationship
15
+ alias_method :belongs_to, :relationship
16
+
14
17
  attr_reader :meta_type, :meta_id, :meta_attributes, :meta_relationships
15
18
  end
16
19
  end
@@ -35,7 +38,7 @@ module JsonapiSerializer::DSL
35
38
  attrs.each do |attr|
36
39
  case attr
37
40
  when Symbol, String
38
- @meta_attributes[attr.to_sym] = lambda { |obj| obj.public_send(attr.to_sym) }
41
+ @meta_attributes[attr.to_sym] = lambda { |obj| obj.public_send(attr) }
39
42
  when Hash
40
43
  attr.each do |key, val|
41
44
  @meta_attributes[key] = lambda { |obj| obj.public_send(val) }
@@ -45,32 +48,21 @@ module JsonapiSerializer::DSL
45
48
  end
46
49
 
47
50
  def attribute(attr, &block)
48
- @meta_attributes[attr.to_sym] = block
51
+ @meta_attributes[attr.to_sym] = block_given? ? block : lambda { |obj| obj.public_send(attr) }
49
52
  end
50
53
 
51
- def has_many(name, opts = {})
52
- @meta_relationships[name] = {
53
- type: :has_many,
54
- from: opts.fetch(:from, name),
55
- serializer: opts[:serializer] || guess_serializer(name.to_s.singularize)
56
- }
57
- end
54
+ def relationship(name, opts = {})
55
+ @meta_relationships[name.to_sym] = {}.tap do |relationship|
56
+ from = opts.fetch(:from, name)
57
+ relationship[:from] = from.respond_to?(:call) ? from : lambda { |r| r.public_send(from) }
58
58
 
59
- def belongs_to(name, opts = {})
60
- @meta_relationships[name] = {
61
- type: :belongs_to,
62
- from: opts.fetch(:from, name),
63
- serializer: opts[:serializer] || guess_serializer(name.to_s)
64
- }
59
+ serializer = opts[:serializer]
60
+ relationship[:serializer] = serializer ? serializer.to_s : "#{name.to_s.singularize.classify}Serializer"
61
+ end
65
62
  end
66
63
 
67
64
  def inherited(subclass)
68
- raise "You attempted to inherit regular serializer class, if you want to create Polymorphic serializer, include Polymorphic mixin"
69
- end
70
-
71
- private
72
- def guess_serializer(name)
73
- "#{name.classify}Serializer".constantize
65
+ raise "You attempted to inherit from #{self.name}, if you want to create Polymorphic serializer, include JsonapiSerializer::Polymorphic"
74
66
  end
75
67
  end
76
68
  end
@@ -25,7 +25,7 @@ module JsonapiSerializer::DSL
25
25
  end
26
26
 
27
27
  def polymorphic_for(*serializers)
28
- @meta_poly += serializers
28
+ @meta_poly += serializers.map(&:to_s)
29
29
  end
30
30
 
31
31
  def inherited(subclass)
@@ -37,7 +37,7 @@ module JsonapiSerializer::DSL
37
37
  @meta_id = parent.meta_id
38
38
  @meta_inherited = true
39
39
  end
40
- @meta_poly << subclass
40
+ @meta_poly << subclass.to_s
41
41
  end
42
42
  end
43
43
  end
@@ -15,14 +15,10 @@ module JsonapiSerializer::Polymorphic
15
15
  def initialize(opts = {})
16
16
  super(opts)
17
17
  unless self.class.meta_inherited
18
- unless self.class.meta_resolver.respond_to? :call
19
- raise "Polymorphic serializer must implement a block resolving an object into type."
20
- end
21
-
22
18
  poly_fields = [*opts.dig(:fields, @type)].map { |f| JsonapiSerializer.key_transform(f) }
23
19
  if self.class.meta_poly.present?
24
20
  @poly = self.class.meta_poly.each_with_object({}) do |poly_class, hash|
25
- serializer = poly_class.new(opts.merge poly_fields: poly_fields)
21
+ serializer = poly_class.constantize.new(opts.merge poly_fields: poly_fields)
26
22
  hash[serializer.type] = serializer
27
23
  end
28
24
  else
@@ -57,6 +53,10 @@ module JsonapiSerializer::Polymorphic
57
53
 
58
54
  private
59
55
  def serializer_for(record)
60
- @poly[self.class.meta_resolver.call(record)] || (raise "Could not resolve serializer for #{record} associated with #{self.class.name}")
56
+ if serializer = @poly[self.class.meta_resolver.call(record)]
57
+ serializer
58
+ else
59
+ raise "Could not resolve serializer for #{record} associated with #{self.class.name}"
60
+ end
61
61
  end
62
62
  end
@@ -22,6 +22,12 @@ module JsonapiSerializer::Utils
22
22
  end
23
23
  end
24
24
 
25
+ def normalize_fields(fields)
26
+ fields.each_with_object({}) do |(type, attributes), hash|
27
+ hash[type.to_sym] = [*attributes].map(&:to_sym)
28
+ end
29
+ end
30
+
25
31
  def apply_splat(item, &block)
26
32
  if item.is_a? Array
27
33
  item.map { |i| block.call(i) }
@@ -1,3 +1,3 @@
1
1
  module JsonapiSerializer
2
- VERSION = "0.1.0"
2
+ VERSION = "0.1.1"
3
3
  end
@@ -1,7 +1,7 @@
1
1
  require 'ffaker'
2
2
 
3
3
  class Book
4
- attr_accessor :id, :name, :isbn, :year, :author, :author_id, :characters, :character_ids, :librarians, :librarian_ids
4
+ attr_accessor :id, :name, :isbn, :year, :author, :author_id, :characters, :character_ids
5
5
 
6
6
  @characters = []
7
7
  @character_ids = []
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: jsonapi_serializer
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ivan Yurov
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-02-22 00:00:00.000000000 Z
11
+ date: 2018-02-27 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -108,6 +108,20 @@ dependencies:
108
108
  - - "~>"
109
109
  - !ruby/object:Gem::Version
110
110
  version: 2.8.1
111
+ - !ruby/object:Gem::Dependency
112
+ name: ruby-prof
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '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'
111
125
  - !ruby/object:Gem::Dependency
112
126
  name: fast_jsonapi
113
127
  requirement: !ruby/object:Gem::Requirement
@@ -132,6 +146,7 @@ files:
132
146
  - ".gitignore"
133
147
  - ".rspec"
134
148
  - ".travis.yml"
149
+ - CHANGELOG.md
135
150
  - CODE_OF_CONDUCT.md
136
151
  - Gemfile
137
152
  - LICENSE.txt
@@ -174,7 +189,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
174
189
  version: '0'
175
190
  requirements: []
176
191
  rubyforge_project:
177
- rubygems_version: 2.5.1
192
+ rubygems_version: 2.7.6
178
193
  signing_key:
179
194
  specification_version: 4
180
195
  summary: Alternative JSONApi serializer