http-api-tools 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +15 -0
  2. data/.gitignore +18 -0
  3. data/.rspec +3 -0
  4. data/Gemfile +6 -0
  5. data/LICENSE.txt +22 -0
  6. data/README.md +485 -0
  7. data/Rakefile +4 -0
  8. data/http-api-tools.gemspec +29 -0
  9. data/lib/hat/base_json_serializer.rb +107 -0
  10. data/lib/hat/expanded_relation_includes.rb +77 -0
  11. data/lib/hat/identity_map.rb +42 -0
  12. data/lib/hat/json_serializer_dsl.rb +62 -0
  13. data/lib/hat/model/acts_like_active_model.rb +16 -0
  14. data/lib/hat/model/attributes.rb +159 -0
  15. data/lib/hat/model/has_many_array.rb +47 -0
  16. data/lib/hat/model/transformers/date_time_transformer.rb +31 -0
  17. data/lib/hat/model/transformers/registry.rb +55 -0
  18. data/lib/hat/model.rb +2 -0
  19. data/lib/hat/nesting/json_serializer.rb +45 -0
  20. data/lib/hat/nesting/relation_loader.rb +89 -0
  21. data/lib/hat/relation_includes.rb +140 -0
  22. data/lib/hat/serializer_registry.rb +27 -0
  23. data/lib/hat/sideloading/json_deserializer.rb +121 -0
  24. data/lib/hat/sideloading/json_deserializer_mapping.rb +27 -0
  25. data/lib/hat/sideloading/json_serializer.rb +125 -0
  26. data/lib/hat/sideloading/relation_sideloader.rb +79 -0
  27. data/lib/hat/sideloading/sideload_map.rb +54 -0
  28. data/lib/hat/type_key_resolver.rb +27 -0
  29. data/lib/hat/version.rb +3 -0
  30. data/lib/hat.rb +9 -0
  31. data/reports/empty.png +0 -0
  32. data/reports/minus.png +0 -0
  33. data/reports/plus.png +0 -0
  34. data/spec/hat/expanded_relation_includes_spec.rb +32 -0
  35. data/spec/hat/identity_map_spec.rb +31 -0
  36. data/spec/hat/model/attributes_spec.rb +170 -0
  37. data/spec/hat/model/has_many_array_spec.rb +48 -0
  38. data/spec/hat/model/transformers/date_time_transformer_spec.rb +36 -0
  39. data/spec/hat/model/transformers/registry_spec.rb +53 -0
  40. data/spec/hat/nesting/json_serializer_spec.rb +173 -0
  41. data/spec/hat/relation_includes_spec.rb +185 -0
  42. data/spec/hat/sideloading/json_deserializer_spec.rb +93 -0
  43. data/spec/hat/sideloading/json_serializer_performance_spec.rb +51 -0
  44. data/spec/hat/sideloading/json_serializer_spec.rb +185 -0
  45. data/spec/hat/sideloading/sideload_map_spec.rb +59 -0
  46. data/spec/hat/support/company_deserializer_mapping.rb +11 -0
  47. data/spec/hat/support/person_deserializer_mapping.rb +9 -0
  48. data/spec/hat/support/spec_models.rb +89 -0
  49. data/spec/hat/support/spec_nesting_serializers.rb +41 -0
  50. data/spec/hat/support/spec_sideloading_serializers.rb +41 -0
  51. data/spec/hat/type_key_resolver_spec.rb +19 -0
  52. data/spec/spec_helper.rb +8 -0
  53. metadata +214 -0
checksums.yaml ADDED
@@ -0,0 +1,15 @@
1
+ ---
2
+ !binary "U0hBMQ==":
3
+ metadata.gz: !binary |-
4
+ ZTExZDgwZjc0ZDQ3M2NiYjlhZDI2NGZlNWIxMWRiOGU2ZGI1MTlkNA==
5
+ data.tar.gz: !binary |-
6
+ YWU4ZTczYzliMjQwZjhjYWJhMDI0MTEzODlmZTIxM2I2ODhlNmVhYQ==
7
+ SHA512:
8
+ metadata.gz: !binary |-
9
+ YzYxODgxNWEyY2YxYmY5MDVhOTdkNTgxNTdkZjUyNmIyOTM2YWJkOGJkNjRk
10
+ YTJiNTYwMzczM2U3NzNmZjczNzU1Y2E5MzczZDk2MDU3ODI5NGE4N2RkNWJi
11
+ YzdjOWI1YjdjYWViMWJjYzFiNTU4MzA0Nzg5MTQxMGRmODk2YmU=
12
+ data.tar.gz: !binary |-
13
+ ZDUyNmQ1YjYxYjI1YTgzN2E0ZjA3ZGI5ZjEzN2VkNTljODM4OTZlODE4M2Jj
14
+ MmM5MWUzM2EwZjk0ODM1YTU1MzlkMDEwYTQwNWZmNjZlZmUzZmVlMzQ0Nzc1
15
+ MmYwNDc1MDNmZDI4NTMxZmM3OTY1ZjRmZmRhZTQwOGVmYTJiNmU=
data/.gitignore ADDED
@@ -0,0 +1,18 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
18
+ reports/profile_report.html
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --color
2
+ --format documentation
3
+ --profile
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # encoding: utf-8
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in hat.gemspec
6
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Hooroo
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,485 @@
1
+ ## NOTE: Current WIP branch with a bunch of refactoring towards adding a nested serializer.
2
+
3
+ -------------------------------------------------------------------------------------------
4
+ Design notes for nested serializer:
5
+
6
+ - The relation_sideloader should be able to become a more generic relation_loader that can support
7
+ loading of both sideloaded and nested structures.
8
+
9
+
10
+
11
+
12
+ # Http API Tools
13
+
14
+ Provides fast serialization/deserialization of models with simple model attribute definition in client apps.
15
+
16
+ Adheres to the ID Based Json API Spec - http://jsonapi.org/format/#id-based-json-api for serialization
17
+
18
+ ## Installation
19
+
20
+ Add this line to your application's Gemfile:
21
+
22
+ gem 'hat'
23
+
24
+ And then execute:
25
+
26
+ $ bundle
27
+
28
+ Or install it yourself as:
29
+
30
+ $ gem install hat
31
+
32
+ ## Usage
33
+ At a high level this gem provides serialization of models (active model or otherwise), deserialization of the serialized json and a way to declaritively create basic models in clients with basic type coercion.
34
+
35
+ It has been written to work as a whole where the producer and client of the api are both maintained by the same development team. Conventions are used throughout to keep things simple. At this stage, breaking these conventions isn't supported in many cases but the gem can be extended towards this goal as the needs arise. Please see the note on performance in the section on contributing at the end of this document.
36
+
37
+ ### Serialization
38
+ There are two supported serialization formats - sideloading and nesting. Both formats maintain an identical api and
39
+ usage pattern while serializing in different ways. While it is possible to provide both formats in an application, it's likely you'd stick to one as the general philosophy is that a resource should always be represented in the same way.
40
+
41
+ To use a serializer in a controller you should instantiate an instance of the serializer for the top level type you're serializing and pass it to render.
42
+
43
+ `render json: UserSerializer.new(user)`
44
+
45
+
46
+ #### Nesting vs Sideloading
47
+ The big difference between these formats is that nesting represents the relationships between resources implicitly in it's structure whereas sideloading is a flattened structure with relationships represented via linked identifiers. The details of these formats will be described in more detail below.
48
+
49
+
50
+ #### Serializer Definition
51
+
52
+ This serializer will either be defined as a nesting or sideloading serializer depening on the serializer it is based on.
53
+
54
+ ```ruby
55
+ class UserSerializer
56
+
57
+ include Hat::Sideloading::JsonSerializer
58
+
59
+ end
60
+ ```
61
+
62
+ ```ruby
63
+ class UserSerializer
64
+
65
+ include Hat::Nesting::JsonSerializer
66
+
67
+ end
68
+
69
+
70
+ Serializers can define attributes and relationships to be serialized.
71
+
72
+ ```ruby
73
+ class UserSerializer
74
+
75
+ include Hat::Sideloading::JsonSerializer
76
+
77
+ serializes(User)
78
+ attributes :id, :first_name, :last_name
79
+ has_many :posts
80
+ has_one :profile
81
+
82
+ end
83
+ ```
84
+
85
+ If you want to serialize any composite attributes they can be defined as a method on the serializer and defined as an attribute. The object being serialized can be accessed via the `serializable` method on the serializer.
86
+
87
+ ```ruby
88
+ class UserSerializer
89
+
90
+ include Hat::Sideloading::JsonSerializer
91
+
92
+ serializes(User)
93
+ attributes :id, :first_name, :last_name, :full_name
94
+
95
+ def full_name
96
+ "#{serializable.first_name} #{serializable.last_name}"
97
+ end
98
+
99
+ end
100
+ ```
101
+
102
+ #### JSON Structure
103
+
104
+ ##### Sideloading
105
+
106
+ By default, only the ids of related objects will be serialized. For serializers using a 'sideloading' approach, these relationships and their ids will be added to the `links` hash.
107
+
108
+
109
+ ```javascript
110
+ {
111
+ "users": [{
112
+ "id": 1,
113
+ "first_name": "John",
114
+ "last_name": "Smith",
115
+ "links" {
116
+ "profile": 2,
117
+ "posts": [3, 4]
118
+ }
119
+ }]
120
+ }
121
+ ```
122
+
123
+ ##### Nesting
124
+ As with sideloading serializers, by default, only the ids of related objects will be serialized. For serializers using a 'nesting' approach, these relationships and their ids will be inlined using their _id / _ids attribute name suffix.
125
+
126
+
127
+
128
+ ```javascript
129
+ {
130
+ "users": [{
131
+ "id": 1,
132
+ "first_name": "John",
133
+ "last_name": "Smith",
134
+ "profile_id": 2,
135
+ "post_ids": [3, 4]
136
+ }]
137
+ }
138
+ ```
139
+
140
+ One advantage to this approach is that it's always clear what relationships exist for a resource, even if you don't
141
+ include the resources themselves in the response.
142
+
143
+ ##### Serializing related resources via includes
144
+ Often it will be desirable to load related data to save on requests. This can be done when creating the top level serializer using the same approach ActiveRecord uses for including relationships in queries.
145
+
146
+ `UserSerializer.new(user).includes(:profile, { posts: [:comments] })`
147
+
148
+ Which produces the following json when sideloaded:
149
+
150
+ ```javascript
151
+ {
152
+ "users": [{
153
+ "id": 1,
154
+ "first_name": "John",
155
+ "last_name": "Smith",
156
+ "links": {
157
+ "profile": 2,
158
+ "posts": [3, 4]
159
+ }
160
+ }],
161
+ "linked": {
162
+ "profiles": [
163
+ {
164
+ "id": 2,
165
+ //...
166
+ }
167
+ ],
168
+
169
+ posts: [
170
+ {
171
+ "id": 3,
172
+ "links": {
173
+ "user": 1,
174
+ "comments": [5]
175
+ }
176
+ //...
177
+ },
178
+ {
179
+ "id": 4,
180
+ "links": {
181
+ "user": 1,
182
+ "comments": []
183
+ }
184
+ //...
185
+ }
186
+ ],
187
+ "comments": [
188
+ "id": 5,
189
+ "links": {
190
+ "post": 3
191
+ }
192
+ //...
193
+ ]
194
+ }
195
+
196
+ }
197
+ ```
198
+
199
+ and the following when nested:
200
+
201
+ ```javascript
202
+ {
203
+ "users": [{
204
+ "id": 1,
205
+ "first_name": "John",
206
+ "last_name": "Smith",
207
+ "profile": {
208
+ "id": 2,
209
+ },
210
+ posts: [
211
+ {
212
+ "id": 3,
213
+ "user_id": 1
214
+ "comments": [
215
+ {
216
+ "id": 5,
217
+ "post_id": 3
218
+ }
219
+ ]
220
+ },
221
+ {
222
+ "id": 4,
223
+ "user_id": 1
224
+ "comments": []
225
+ }
226
+ ]
227
+ }]
228
+ }
229
+
230
+ One benefit to sideloading over nesting resources is that if the same resource is referenced multiple times, it only needs to be serialized once. Depending on your data, this may or may not be significant.
231
+
232
+ ##### Including related resources via the url
233
+ It's possible to determine what resources to include by providing a query string parameter:
234
+
235
+ `http://example.com/users/1?include?comments,posts.comments`
236
+
237
+ This can be parsed using:
238
+
239
+ `relation_includes = Hat::RelationIncludes.from_params(params)`
240
+
241
+ and splat into the serializer includes:
242
+
243
+ `UserSerializer.new(user).includes(*relation_includes)`
244
+
245
+ and/or active record queries:
246
+
247
+ `User.find(params[:id]).includes(*user_serializer.includes_for_query)`
248
+
249
+ When providing the includes for an active record query, we actually want a deeper set of includes in order to account for the ids fetched for has_many relationships. If we passed the same set of includes to the query as we pass to the serializer, we'd end up with n+1 queries when fetching the ids for the has_many relationships.
250
+
251
+ Calling `user_serializer.includes_for_query` will figure out the minimum set of includes that are required based on the following:
252
+
253
+ * The models and their relationships
254
+ * The relationships actually being serialized
255
+
256
+ ##### Restricting what is included
257
+ Once you expose what can be included as a query string parameter you risk exposing too much information or poorly considered api calls that fetch too much. This can be countered by defining what is `includable` for each serializer when it's being used as the root serializer for a json response.
258
+
259
+ ```ruby
260
+ class UserSerializer
261
+
262
+ include Hat::Nesting::JsonSerializer
263
+
264
+ serializes(User)
265
+
266
+ attributes :id, :first_name, :last_name, :full_name
267
+
268
+ has_many :posts
269
+ has_many :comments
270
+
271
+ includable(:profile, {:posts, [:comments]})
272
+
273
+ end
274
+ ```
275
+
276
+ This will ensure that regardless of what is declared in the `include` param, no more than the allowable includes are ever returned.
277
+
278
+ To help in documenting what is includable, both the includable and included relations are returned in the meta data of the response.
279
+
280
+ ```javascript
281
+ "meta": {
282
+ "type": "user",
283
+ "root_key": "users",
284
+ "includable": "profile,posts,posts.comments"
285
+ "included": "posts"
286
+ }
287
+ ```
288
+
289
+ #### Meta data
290
+ Every request will also contain a special meta attribute which could be augmented with various additional pieces
291
+ of meta-data. At this point, it will always return the `type` and `root_key` for the current request. Eg:
292
+
293
+ ```javascript
294
+ {
295
+ "meta": {
296
+ "type": "user",
297
+ "root_key": "users"
298
+ },
299
+ "users": [{
300
+ "id": 1,
301
+ "first_name": "John",
302
+ "last_name": "Smith",
303
+ "profile_id": 2,
304
+ "post_ids": [3, 4]
305
+ }]
306
+ }
307
+ ```
308
+
309
+ Notice that the root is an array and the root_key a plural. This is the case regardless of whether a single resource
310
+ is being represented or a collection of resources. This is in line with the json-api spec and generally simplifies both serialization and deserialization.
311
+
312
+ ##### Adding Metadata
313
+ It might be desirable to add extra metadata to the serialized response. For example, adding information such as limit, offset, what includes are valid etc can be helpful to a client.
314
+
315
+ `UserSerializer.new(user).meta(limit: 10, offset: 0)`
316
+
317
+
318
+
319
+ ### Deserialization
320
+ The `Hat::JsonDeserializer` expects json in the format that the serializer has created making it easy to create matching rest apis and clients with little work needing to be done at each end. Currently only sideloaded json can be deserialized. Nested deserializers are coming.
321
+
322
+ `Hat::JsonDeserializer.new(json).deserialize`
323
+
324
+ This will iterate over the json, using the attribute names to match types to models in the client app. As long as models exist with names that match the keys in the json, a complete graph of objects will be created upon deserialization, complete with two way relationships when they exist.
325
+
326
+ In the previous example, the following model classes would be expected:
327
+
328
+ * User
329
+ * Post
330
+ * Comment
331
+
332
+ #### Deserializer Mappings
333
+
334
+ At times, the name of an object's key may deviate from it's type and can't be deserialized by convention alone.
335
+
336
+ ```javascript
337
+ {
338
+ "users": [{
339
+ "id": 1,
340
+ "first_name": "John",
341
+ "last_name": "Smith",
342
+ "links": {
343
+ "posts": [3]
344
+ }
345
+ }],
346
+ "linked": {
347
+ posts: [
348
+ {
349
+ "id": 3,
350
+ "links": {
351
+ "author": 1
352
+ }
353
+ }
354
+ }
355
+ }
356
+ ```
357
+
358
+ In this example, the `user` is the `author` of the `post`. It is impossible to infer from the data that an `author` attribute key should map to a `User` type so we need to give it a helping hand. This can be done once per type by creating a `JsonDeserializerMapping` class. Like with serializers, deserializer mappings are convention based, using the model class name as a prefix.
359
+
360
+ ```ruby
361
+ class PostDeserializerMapping
362
+
363
+ include Hat::JsonDeserializerMapping
364
+
365
+ map :author, User
366
+
367
+ end
368
+ ```
369
+
370
+ Whenever we're deserializing a `post`, the `author` attribute will always be deserialized to an instance of a `User`.
371
+
372
+ This can also be applied against collections:
373
+
374
+ ```ruby
375
+ class CompanyDeserializerMapping
376
+
377
+ include Hat::JsonDeserializerMapping
378
+
379
+ map :employees, Person
380
+
381
+ end
382
+ ```
383
+
384
+ ### Models
385
+ Client models have some basic requirements that are catered to such as attribute definition, default values and type tranforms.
386
+
387
+ For example:
388
+
389
+ ```ruby
390
+ class User
391
+
392
+ include Hat::Model::Attributes
393
+ include Hat::Model::ActsLikeActiveModel
394
+
395
+ attribute :id
396
+ attribute :first_name
397
+ attribute :last_name
398
+ attribute :created_at: type: :date_time
399
+ attribute :posts, default: []
400
+ attribute :profile
401
+
402
+ end
403
+ ```
404
+
405
+ This will define a User class with attr_accessors for all attributes defined. The initialize method will accept a hash of values which will be passed through type transformers when configured and have defaults applied when no value is passed in for a key.
406
+
407
+ Currently there is a single registered type transform for date_time transforms. This expects an iso8601 date format as a string which will be transformed into a ruby DateTime.
408
+
409
+ #### Registering custom type transformers.
410
+
411
+ Type transformers expect the following two-way interface:
412
+
413
+ ```ruby
414
+ class MoneyTranformer
415
+
416
+ def self.from_raw(value)
417
+ Money.new(value)
418
+ end
419
+
420
+ def self.to_raw(money)
421
+ money.to_s
422
+ end
423
+
424
+ end
425
+ ```
426
+
427
+ Transformers should then be registered against a type key:
428
+
429
+
430
+ ```ruby
431
+ Hat::Transformers::Registry.instance.register(:money, MoneyTransformer)
432
+ ```
433
+
434
+ Now you can define an attribute as a `money` type:
435
+
436
+ ```ruby
437
+ class Account
438
+
439
+ include Hat::Model::Attributes
440
+
441
+ attribute :balance: type: :money
442
+
443
+ end
444
+ ```
445
+
446
+ #### Read only attributes
447
+ Sometimes it's useful to define a field as readonly. The intent being that we prevent changing an attribute value that shouldn't be changed or prevent a value from being serialized and sent in the payload that the server won't accept.
448
+
449
+ In the previous example, it might be better to set the `created_at` field as readonly:
450
+
451
+ ```ruby
452
+ class User
453
+
454
+ include Hat::Model::Attributes
455
+ include Hat::Model::ActsLikeActiveModel
456
+
457
+ attribute :id
458
+ attribute :first_name
459
+ attribute :last_name
460
+ attribute :created_at: type: :date_time, read_only: true
461
+ attribute :posts, default: []
462
+ attribute :profile
463
+
464
+ end
465
+ ```
466
+
467
+ ### Polymorphism
468
+ At this point, polymorphic relationships are not catered for but they can be when the need arises.
469
+
470
+
471
+ ## Contributing
472
+
473
+ ### A note on performance
474
+ Performance is critial for this gem so any changes must be made with this in mind. There is a basic performance
475
+ spec for serialization that dumps some timings and creates a profile report in `reports/profile_report.html`.
476
+
477
+ Until we have a more robust way of tracking performance over time, please do some before and after tests against this when you make changes. Even small things have been found to introduce big performance issues.
478
+
479
+
480
+ ## To Do
481
+ * Deserializer for nested json
482
+
483
+
484
+
485
+
data/Rakefile ADDED
@@ -0,0 +1,4 @@
1
+ require "bundler/gem_tasks"
2
+ require 'rspec/core/rake_task'
3
+ task :default => :spec
4
+ RSpec::Core::RakeTask.new
@@ -0,0 +1,29 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'hat/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "http-api-tools"
8
+ spec.version = Hat::VERSION
9
+ spec.authors = ["Rob Monie"]
10
+ spec.email = ["robmonie@gmail.com"]
11
+ spec.description = %q{Http API Tools}
12
+ spec.summary = %q{Provides JSON serialization/deserialization and basic model attribute definition for client apps}
13
+ spec.homepage = ""
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files`.split($/)
17
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.3"
22
+ spec.add_development_dependency "rake"
23
+ spec.add_development_dependency "activesupport"
24
+ spec.add_development_dependency "rspec"
25
+ spec.add_development_dependency "pry"
26
+ spec.add_development_dependency "pry-debugger"
27
+ spec.add_development_dependency "ruby-prof"
28
+
29
+ end
@@ -0,0 +1,107 @@
1
+
2
+ require "active_support/core_ext/class/attribute"
3
+ require "active_support/json"
4
+ require 'active_support/core_ext/string/inflections'
5
+ require_relative 'relation_includes'
6
+ require_relative 'identity_map'
7
+ require_relative 'type_key_resolver'
8
+
9
+ module Hat
10
+ module BaseJsonSerializer
11
+
12
+ attr_reader :serializable, :relation_includes, :result, :meta_data
13
+
14
+ def initialize(serializable, attrs = {})
15
+ @serializable = serializable
16
+ @relation_includes = attrs[:relation_includes] || RelationIncludes.new
17
+ @result = attrs[:result] || {}
18
+ @meta_data = { type: root_key.to_s.singularize, root_key: root_key.to_s }
19
+ end
20
+
21
+
22
+ def attribute_hash
23
+
24
+ attribute_hash = {}
25
+
26
+ attributes.each do |attr_name|
27
+ if self.respond_to?(attr_name)
28
+ attribute_hash[attr_name] = self.send(attr_name)
29
+ else
30
+ attribute_hash[attr_name] = serializable.send(attr_name)
31
+ end
32
+ end
33
+
34
+ attribute_hash
35
+
36
+ end
37
+
38
+
39
+ def to_json(*args)
40
+ JSON.fast_generate(as_json)
41
+ end
42
+
43
+ def includes(*includes)
44
+
45
+ if includable
46
+ allowable_includes_to_add = RelationIncludes.new(*includes) & includable
47
+ else
48
+ allowable_includes_to_add = includes
49
+ end
50
+
51
+ relation_includes.include(allowable_includes_to_add)
52
+
53
+ self
54
+ end
55
+
56
+ def meta(data)
57
+ meta_data.merge!(data)
58
+ self
59
+ end
60
+
61
+ def attributes
62
+ self.class._attributes
63
+ end
64
+
65
+ def has_ones
66
+ self.class.has_ones
67
+ end
68
+
69
+ def has_manys
70
+ self.class.has_manys
71
+ end
72
+
73
+ def includable
74
+ self.class._includable
75
+ end
76
+
77
+ def includes_for_query
78
+ RelationIncludes.new(*ExpandedRelationIncludes.new(relation_includes, self))
79
+ end
80
+
81
+ protected
82
+
83
+ attr_accessor :identity_map
84
+
85
+ private
86
+
87
+ attr_writer :relation_includes
88
+ attr_reader :type_key_resolver
89
+ attr_accessor :serializer_map, :meta_data
90
+
91
+ def root_key
92
+ @_root_key ||= self.class._serializes.name.split("::").last.underscore.pluralize.to_sym
93
+ end
94
+
95
+ def includes_meta_data
96
+ {
97
+ includable: includable.to_s.blank? ? '*' : includable.to_s,
98
+ included: relation_includes.to_s
99
+ }
100
+ end
101
+
102
+ def assert_id_present(serializable_item)
103
+ raise "serializable items must have an id attribute" unless serializable_item.respond_to?(:id)
104
+ end
105
+
106
+ end
107
+ end