simple_ams 0.2.5 → 0.2.6

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +28 -0
  3. data/.rubocop.yml +56 -0
  4. data/CHANGELOG.md +22 -0
  5. data/Gemfile +2 -2
  6. data/README.md +663 -116
  7. data/Rakefile +3 -3
  8. data/bin/console +3 -3
  9. data/lib/simple_ams.rb +34 -33
  10. data/lib/simple_ams/adapters/ams.rb +26 -32
  11. data/lib/simple_ams/adapters/jsonapi.rb +46 -65
  12. data/lib/simple_ams/document.rb +38 -28
  13. data/lib/simple_ams/document/fields.rb +36 -37
  14. data/lib/simple_ams/document/forms.rb +7 -9
  15. data/lib/simple_ams/document/generics.rb +35 -37
  16. data/lib/simple_ams/document/links.rb +7 -9
  17. data/lib/simple_ams/document/metas.rb +7 -11
  18. data/lib/simple_ams/document/primary_id.rb +14 -17
  19. data/lib/simple_ams/document/relations.rb +99 -109
  20. data/lib/simple_ams/dsl.rb +73 -71
  21. data/lib/simple_ams/methy.rb +2 -2
  22. data/lib/simple_ams/options.rb +268 -266
  23. data/lib/simple_ams/options/adapter.rb +2 -2
  24. data/lib/simple_ams/options/concerns/filterable.rb +29 -34
  25. data/lib/simple_ams/options/concerns/mod.rb +4 -0
  26. data/lib/simple_ams/options/concerns/name_value_hash.rb +25 -26
  27. data/lib/simple_ams/options/concerns/tracked_properties.rb +15 -17
  28. data/lib/simple_ams/options/concerns/value_hash.rb +25 -26
  29. data/lib/simple_ams/options/fields.rb +1 -1
  30. data/lib/simple_ams/options/forms.rb +1 -2
  31. data/lib/simple_ams/options/generics.rb +2 -4
  32. data/lib/simple_ams/options/includes.rb +1 -1
  33. data/lib/simple_ams/options/links.rb +1 -1
  34. data/lib/simple_ams/options/metas.rb +1 -1
  35. data/lib/simple_ams/options/primary_id.rb +1 -1
  36. data/lib/simple_ams/options/relations.rb +9 -7
  37. data/lib/simple_ams/options/type.rb +1 -2
  38. data/lib/simple_ams/renderer.rb +43 -41
  39. data/lib/simple_ams/version.rb +1 -1
  40. data/simple_ams.gemspec +17 -16
  41. metadata +38 -21
  42. data/.travis.yml +0 -15
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 36fd11b2f28ec9693e791016218c919a970d2aa607055db3e4899ceb0b6948ce
4
- data.tar.gz: aae4f29ddec009c20e5cf14b4c2ff9d6c8ec4c68e2c5905c5c58a68a8137b710
3
+ metadata.gz: d2221682d109248b00a4d4d8a99b8bac469cd76ec60ad4e5710c9b56e43bbf5f
4
+ data.tar.gz: 0aa48e2d0e09dd36cc8b31ef3daff832059e5d01ea5b090648def84c677de036
5
5
  SHA512:
6
- metadata.gz: 7568f8f6f5e8983e98e08c9212c27cff0f3496ef7e9a95055b4dd042e461fb915e084b7e7eb8ac089a2e6bd06bc05bd2d4cb9bcba3baa73b351d0a34ef85ef3a
7
- data.tar.gz: c199d83390e75bea8b44241bc49000aa28bd9dc60535fd243d3b59351e795c1341b23811688b05bd59790b07b1426400b8f665f43033f2ecc5aedcf9ba05be23
6
+ metadata.gz: e8355af588f6efa58699e0394d3d913c56e16d1573e334930512f971c80a20f285e8200c9d74bd634e07a81de5e79915d7bd694afe6e76161f8b9a592c4a0896
7
+ data.tar.gz: 38f6bbb2a0493870544866eb04a9501db97232f97b02dd647aeeea83f336e2cf8ae8d7979553aa74ad8cab7d409e06cabec6949b375080d3c96d850f290654fd
@@ -0,0 +1,28 @@
1
+ name: Ruby
2
+
3
+ on:
4
+ push:
5
+ branches: [ master ]
6
+ pull_request:
7
+ branches: [ master ]
8
+
9
+ jobs:
10
+ test:
11
+ strategy:
12
+ fail-fast: false
13
+ matrix:
14
+ os: [ ubuntu-latest ]
15
+ ruby: [ 2.5, 2.6, 2.7, '3.0', truffleruby ]
16
+ runs-on: ${{ matrix.os }}
17
+ steps:
18
+ - uses: actions/checkout@v2
19
+ - name: Setup Ruby
20
+ uses: ruby/setup-ruby@v1
21
+ with:
22
+ ruby-version: ${{ matrix.ruby }}
23
+ - name: Install dependencies
24
+ run: bundle install
25
+ - name: Rubocop
26
+ run: bundle exec rubocop
27
+ - name: Run tests
28
+ run: bundle exec rspec spec
data/.rubocop.yml ADDED
@@ -0,0 +1,56 @@
1
+ AllCops:
2
+ NewCops: enable
3
+ Style/FrozenStringLiteralComment:
4
+ Enabled: false
5
+ Style/ClassAndModuleChildren:
6
+ EnforcedStyle: compact
7
+ Layout/FirstHashElementIndentation:
8
+ EnforcedStyle: consistent
9
+ Metrics/ModuleLength:
10
+ Max: 250
11
+ Metrics/ClassLength:
12
+ Max: 250
13
+ Metrics/MethodLength:
14
+ Max: 50
15
+ Metrics/BlockLength:
16
+ Max: 50
17
+ Exclude:
18
+ - 'spec/**/*.rb'
19
+ Metrics/AbcSize:
20
+ Max: 50
21
+ Lint/UnreachableLoop:
22
+ Exclude:
23
+ - 'spec/**/*.rb'
24
+ Style/Documentation:
25
+ Enabled: false
26
+ Lint/UnderscorePrefixedVariableName:
27
+ Exclude:
28
+ - 'spec/**/*.rb'
29
+ - 'lib/simple_ams/options.rb'
30
+ - 'lib/simple_ams/dsl.rb'
31
+ Naming/MemoizedInstanceVariableName:
32
+ Exclude:
33
+ - 'lib/simple_ams/options.rb'
34
+ - 'lib/simple_ams/dsl.rb'
35
+ Lint/SuppressedException:
36
+ Exclude:
37
+ - 'spec/**/*.rb'
38
+ Metrics/PerceivedComplexity:
39
+ Max: 14
40
+ Exclude:
41
+ - 'spec/**/*.rb'
42
+ Metrics/CyclomaticComplexity:
43
+ Max: 14
44
+ Exclude:
45
+ - 'spec/**/*.rb'
46
+ Lint/ConstantDefinitionInBlock:
47
+ Exclude:
48
+ - 'spec/**/*.rb'
49
+ Style/OptionalArguments:
50
+ Exclude:
51
+ - 'lib/simple_ams/options/relations.rb'
52
+ Naming/PredicateName:
53
+ Exclude:
54
+ - 'lib/simple_ams/dsl.rb'
55
+ Lint/MissingSuper:
56
+ Enabled: false
data/CHANGELOG.md ADDED
@@ -0,0 +1,22 @@
1
+ **0.2.6**
2
+ * [#45](https://github.com/vasilakisfil/SimpleAMS/pull/45) add changelog
3
+ * [#44](https://github.com/vasilakisfil/SimpleAMS/pull/44) add Ruby 3.0 support
4
+ * [#42](https://github.com/vasilakisfil/SimpleAMS/pull/42) fix bug when more than 1 nested relations where specified on Renderer
5
+ * [#37](https://github.com/vasilakisfil/SimpleAMS/pull/37) Use Rubocop to lint
6
+ * [#34](https://github.com/vasilakisfil/SimpleAMS/pull/34) update README with detailed documentation and examples
7
+ * [#31](https://github.com/vasilakisfil/SimpleAMS/pull/31) improve serializer inference when collection is not an array
8
+
9
+ **v0.2.5**
10
+ * [#28](https://github.com/vasilakisfil/SimpleAMS/pull/28) fix repo reference in gemspec
11
+ * [#27](https://github.com/vasilakisfil/SimpleAMS/pull/27) fixes bug when rendering polymorphic collections (properly cleans serializer variable)
12
+
13
+ **v0.2.3**
14
+ * [#24](https://github.com/vasilakisfil/SimpleAMS/pull/24) add travis for CI
15
+ * fixes on JSON:API adapter (`skip_data` option)
16
+ * small performance improvements
17
+
18
+ **0.2.1**
19
+ * various bug fixes and performance improvements
20
+
21
+ **0.1.5**
22
+ * first stable release
data/Gemfile CHANGED
@@ -1,6 +1,6 @@
1
- source "https://rubygems.org"
1
+ source 'https://rubygems.org'
2
2
 
3
- git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
3
+ git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }
4
4
 
5
5
  # Specify your gem's dependencies in SimpleAMS.gemspec
6
6
  gemspec
data/README.md CHANGED
@@ -6,6 +6,43 @@
6
6
  If we want to interact with modern APIs we should start building modern, flexible libraries
7
7
  that help developers to build such APIs. Modern Ruby serializers, as I always wanted them to be.
8
8
 
9
+ You can find the core ideas, the reasoning behind the architecture, use cases
10
+ and examples [here](https://vasilakisfil.social/blog/2020/01/20/modern-ruby-serializers/).
11
+
12
+ ## Table of contents
13
+
14
+ 1. [Installation](#installation)
15
+ 2. [Usage](#usage)
16
+ * [Simple case](#simple-case)
17
+ - [Rendering a resource](#rendering-a-resource)
18
+ - [Rendering a collection](#rendering-a-collection)
19
+ * [Serializer DSL](#serializer-dsl)
20
+ - [fields directive](#fields-directive)
21
+ - [Relations (has_many/has_one/belongs_to)](#relations-has_manyhas_onebelongs_to)
22
+ * [relations are recursive](#relations-are-recursive)
23
+ * [embedded content (again recursive)](#embedded-content-again-recursive)
24
+ * [relation name/type](#relation-nametype)
25
+ - [value-hashmap type of directives](#value-hashmap-type-of-directives)
26
+ * [adapter](#adapter)
27
+ * [primary_id](#primary_id)
28
+ * [type](#type)
29
+ - [name-value-hashmap type of directives](#name-value-hashmap-type-of-directives)
30
+ * [link](#link)
31
+ * [meta](#meta)
32
+ * [form](#form)
33
+ * [generic](#generic)
34
+ * [group of link/meta/form/generic](#group-of-linksmetasformsgenerics)
35
+ - [collection directive](#collection-directive)
36
+ * [Rendering DSL](#rendering-dsl)
37
+ - [includes vs relations](#includes-vs-relations)
38
+ - [Rendering collections](#rendering-collections)
39
+ - [Rendering options with values](#rendering-options-with-values)
40
+ - [Exposing methods inside the serializer, like helpers](#exposing-methods-inside-the-serializer-like-helpers)
41
+ * [Extended DSL show off](#extended-dsl-show-off)
42
+ 3. [Development](#development)
43
+ 4. [Contributing](#contributing)
44
+
45
+
9
46
  ## Installation
10
47
 
11
48
  Add this line to your application's Gemfile:
@@ -23,103 +60,651 @@ Or install it yourself as:
23
60
  $ gem install simple_ams
24
61
 
25
62
  ## Usage
26
- The gem's interface has been inspired by ActiveModel Serializers 0.9.2, 0.10.stable, jsonapi-rb and Ember Data.
27
- However, **it has been built for POROs, does not rely in any dependency and does not relate to Rails in any case** other than
28
- some nostalgia for the (advanced at that time) pre-0.10 ActiveModel Serialiers.
63
+ The gem's interface has been inspired by ActiveModel Serializers 0.9.2,
64
+ 0.10.stable, jsonapi-rb and Ember Data.
65
+ However, it has been built for POROs, **has zero dependencies** and does not
66
+ relate to Rails in any case other than some nostalgia for the (advanced at that
67
+ time) pre-0.10 ActiveModel Serialiers.
29
68
 
69
+ You can find the core ideas, the reasoning behind the architecture, use cases and examples [here](https://vasilakisfil.social/blog/2020/01/20/modern-ruby-serializers/).
30
70
 
31
71
  ### Simple case
32
-
33
72
  You will rarely need all the advanced options. Usually you will have something like that:
34
73
 
35
74
  ```ruby
36
75
  class UserSerializer
37
76
  include SimpleAMS::DSL
38
77
 
39
- #specify the adapter, pass some options all the way down to the adapter
40
- adapter SimpleAMS::Adapters::JSONAPI, root: true
41
-
42
- #specify available attributes/fields
43
- attributes :id, :name, :email, :birth_date
44
-
45
- #specify available relations
46
- has_one :profile, serializer: ProfileSerializer
47
- #belongs_to is just an alias to has_one
48
- belongs_to :organization, serializer: OrganizationSerializer
49
- has_many :videos, serializer: VideosSerializer do
50
- #rarely used: if you need more options, you can pas a block
51
- #which adheres to the same DSL as described here
52
- #it goes to an option called `embedded`
53
- #essentially these options here should be used for linking current resource
54
- #with the relation (useful for JSONAPI for instance)
55
- generic :include_data, false
56
- end
78
+ #specify the adapter we want to use
79
+ adapter SimpleAMS::Adapters::JSONAPI
57
80
 
58
- #specify some links
59
- link :feed, '/api/v1/me/feed'
60
- #links can also take other options, as specified by RFC 8288
61
- link :root, '/api/v1/', rel: :user
62
- #link values can be dynamic as well through lambdas
63
- #lambdas take arguments the object to be serialized and the instantiated serializer
64
- link :posts, ->(obj, s) { s.api_v1_user_followers_path(user_id: obj.id) }, rel: :user
65
- #if you also need dynamic options, you can return an array from the lambda
66
- link :followers, ->(obj, s) { ["/api/v1/users/#{obj.id}/followers/", rel: obj.type] }
67
-
68
- #same with metas: can be static, dynamic and accept arbitrary options
69
- meta :environment, ->(obj, s) { Rails.env.to_s }
70
-
71
- #same with form: can be static, dynamic and accept arbitrary options
72
- form :create, ->(obj, s) { User::CreateForm.for(obj) }
73
-
74
- #or if you need something quite generic (and probably adapter-related)
75
- #again it follows the same patterns as link
76
- generic :include_embedded_data, true, {only: :collection}
77
-
78
- #these are properties to the collection resource itself
79
- #AND NOT to each resource separately, when applied inside a collection..
80
- #It's a rarely used feature but definitely nice to have..
81
- collection do
82
- #collection accepts exactly the same aforementioned interface
83
- #here we use only links and meta
84
- link :root, '/api/v1/', rel: :user
85
- type :users
86
- meta :count, ->(collection, s) { collection.count }
87
- end
88
-
89
- #note that most probably the only thing that you will need here is the `type`,
90
- #so there is a shortcut if you just need to specify the collection name/type:
91
- #collection :users
81
+ #specify the attributes we want to serialize from the given object
82
+ attributes :id, :name, :email, :created_at, :role
92
83
 
93
- #override an attribute
94
- def name
95
- "#{object.first_name} #{object.last_name}"
96
- end
84
+ #specify the type of the resource
85
+ type :user
86
+ #specify the name of the collection
87
+ collection :users
97
88
 
98
- #override a relation
99
- def videos
100
- Videos.where(user_id: object.id).published
101
- end
89
+ #specify a relation. Here microposts serves as both a name of the collection
90
+ #and the name of the method used to retrieve the values of the collection
91
+ #from the given object
92
+ has_many :microposts
102
93
  end
103
94
  ```
104
95
 
105
- Then you can just feed your serializer with data, along with some options:
96
+
97
+ #### Rendering a resource
98
+ Then you can just feed your serializer with data:
106
99
 
107
100
  ```ruby
108
- SimpleAMS::Renderer.new(user, fields: [:id, :name, :email], includes: [:videos]).to_json
101
+ SimpleAMS::Renderer.new(user).to_json
109
102
  ```
110
103
  `to_json` first calls `as_json`, which creates a ruby Hash and then `to_json` is called
111
104
  on top of that hash.
112
105
 
106
+ If you want to filter the available options (defined by the serializer) when you
107
+ instantiate the serializer, `Renderer` accepts an options hash. In there you can
108
+ throw pretty much the same DSL:
109
+
110
+ ```ruby
111
+ SimpleAMS::Renderer.new(user, {
112
+ serializer: UserSerializer, fields: [:id, :name, :email], includes: []
113
+ }).to_json
114
+ ```
115
+
116
+ Here we say that we only want 3 specific fields, and no relations at all.
117
+
118
+
119
+ #### Rendering a collection
120
+ Rendering a collection is pretty similar, meaning that it reuses the same serializer
121
+ class, and accepts the same runtime options. The only difference is that you need
122
+ to call a different class.
123
+
124
+ ```ruby
125
+ SimpleAMS::Renderer::Collection.new(users, {
126
+ serializer: UserSerializer, fields: [:id, :email, :name], includes: []
127
+ }).to_json
128
+ ```
129
+
130
+ ### Serializer DSL
131
+ The serializer is a very robust, yet simple, with a hash-based internal
132
+ representation.
133
+
134
+ #### fields directive
135
+ Fields specify the attributes that the serializer will hold.
136
+ The values of each attribute is taken by the to-be serialized object,
137
+ unless the serializer has a method of the same name.
138
+
139
+ ```ruby
140
+ fields :id, :name, :email, :created_at, :role
141
+ ```
142
+
143
+
144
+ Using `attributes` is also valid, it’s just an [alias](https://github.com/vasilakisfil/SimpleAMS/blob/master/lib/simple_ams/dsl.rb#L130-L137) after all:
145
+
146
+ ```ruby
147
+ attributes :id, :name, :email, :created_at, :role
148
+ ```
149
+
150
+
151
+ Of course, any field can be overridden by defining a method of the same name
152
+ inside the serializer.
153
+ In there, you can have access to a method called object which holds the actual
154
+ resource to be serialized:
155
+
156
+ ```ruby
157
+ def name
158
+ "#{object.first_name} #{object.last_name}"
159
+ end
160
+ ```
161
+
162
+
163
+ #### Relations (has_many/has_one/belongs_to)
164
+ These directives allows you to append relations in a resource.
165
+ `has_one` is just an alias of `belongs_to` since there is no real difference in
166
+ APIs (although internally and in adapters, SimpleAMS knows if you specified the
167
+ relation using`belongs_to` or `has_one`, making it future proof in case API specs
168
+ decide to support each one in a different way).
169
+
170
+ ```ruby
171
+ has_many :microposts
172
+ ```
173
+
174
+ Again, it can be overridden by defining a method of the same name:
175
+
176
+ ```ruby
177
+ def microposts
178
+ Post.where(user_id: object.id).order(:created_at, :desc).limit(10)
179
+ end
180
+ ```
181
+
182
+ ##### relations are recursive
183
+ The relations directives can take the same options as the rendering.
184
+
185
+ ```ruby
186
+ #overriding the serializer
187
+ has_many :microposts, serializer: CustomPostsSerializer
188
+ #overriding the serializer and fields that should be included
189
+ has_many :microposts, serializer: CustomPostsSerializer, fields: [:content]
190
+ #overriding the serializer, fields and relations that should be included
191
+ has_many :microposts, serializer: CustomPostsSerializer, fields: [:content],
192
+ includes: []
193
+ #overriding the serializer, fields, relations and links
194
+ has_many :microposts, serializer: CustomPostsSerializer, fields: [:content],
195
+ includes: [], links: [:self]
196
+ ```
197
+
198
+
199
+ When overriding from the relations directives (or when rendering in general) you
200
+ are able to override any directive defined in the serializer to acquire a subset
201
+ **but never a superset**.
202
+
203
+ ##### embedded content (again recursive)
204
+ Sometimes, an annoying spec might define parts of a relation in the main body,
205
+ while parts of the relation somewhere else. For instance, JSON:API does that by
206
+ having some links in the main body and the rest in the included section.
207
+ That’s also possible if you pass a block in the relation directive:
208
+
209
+ ```ruby
210
+ has_many :microposts, serializer: MicropostsSerializer, fields: [:content] do
211
+ #these goes to a class named `Embedded`, attached to the relation
212
+ link :self, ->(obj){ "/api/v1/users/#{obj.id}/relationships/microposts" }
213
+ link :related, ->(obj){ ["/api/v1/users/1", rel: :user] }
214
+ end
215
+ ```
216
+
217
+ Inside that block, you can pass any parameter the original DSL supports and will
218
+ be stored in an Embedded class under MicropostsSerializer.
219
+ Btw SimpleAMS is smart enough (one of the very few cases that acts like that) to
220
+ figure out that if a lambda returns something that’s not an array, then this must
221
+ be the value, while options are just empty.
222
+
223
+ ##### relation name/type
224
+ Sometimes, we want to detach the relation’s name from the type. In the previous
225
+ example `microposts` is the relation name (whatever that means), while the type
226
+ is defined by the `MicropostsSerializer`, unless we override it, which can be
227
+ done either in the relation serializer itself, or when we use the relation from
228
+ the parent serializer:
229
+
230
+ ```ruby
231
+ has_many :microposts, serializer: MicropostsSerializer, fields: [:content], type: :feed do
232
+ link :self, ->(obj){ "/api/v1/users/#{obj.id}/relationships/microposts" }
233
+ link :related, ->(obj){ ["/api/v1/users/1", rel: :user] }
234
+ end
235
+ ```
236
+
237
+ Internally SimpleAMS, differentiates type from name, and usually type is
238
+ something that’s semantically stronger (like a relation type) than name.
239
+ You can even inject the name of the relation using the name option:
240
+
241
+ ```ruby
242
+ has_many :microposts, serializer: MicropostsSerializer, fields: [:content], type: :feed, name: :posts do
243
+ link :self, ->(obj){ "/api/v1/users/#{obj.id}/relationships/microposts" }
244
+ link :related, ->(obj){ ["/api/v1/users/1", rel: :user] }
245
+ end
246
+ ```
247
+
248
+ As I said, the name, which is usually the name of the attribute that includes
249
+ the relation in the JSON format, doesn’t really have any semantic meaning in
250
+ most specs. At least I haven’t seen any spec to depend on the root attribute
251
+ name of the relation. Instead it’s the type that’s important, because type is
252
+ what the [web linking RFC defines](https://tools.ietf.org/html/rfc8288#section-2).
253
+
254
+ #### value-hashmap type of directives
255
+ These are directives like adapter. They take a value, and optionally a hashmap,
256
+ which are options to be passed down straight to the adapter, hence they are adapter specific.
257
+ Such options are `primary_id`, `type` and `adapter`
258
+
259
+ For instance, for adapter it could be:
260
+ ```ruby
261
+ adapter SimpleAMS::Adapters::JSONAPI, root: true
262
+ ```
263
+
264
+ Of course, since we are talking about Ruby here, it would be a huge restriction
265
+ to not allow dynamic value/hashmap combination. Basically any such directive
266
+ can accept a lambda (generally anything that responds to `call`) and should
267
+ return an array where the first part is the value and (optionally) the second part is the
268
+ options. There is an argument that is passed down to the function/lambda, and
269
+ that’s the actual object. For instance, to support polymorphic resources you
270
+ can have the type dynamic:
271
+
272
+ ```ruby
273
+ type ->(obj, s){ obj.employee? ? [:employee, {}] : [:user, {}]}
274
+ ```
275
+
276
+ One of the very few times that SimpleAMS acts smart is inside the lambda, that
277
+ if you have only a value (not an Array), it will take that as the value, while
278
+ the options will be taken by the second argument, after the lambda. So the
279
+ above is equivalent with:
280
+
281
+ ```ruby
282
+ type ->(obj, s){ obj.employee? ? :employee : :user}, {}
283
+ ```
284
+
285
+ Note: you shouldn't use that in case of adapter, as that's the definition of UB :P
286
+
287
+
288
+ <details>
289
+ <summary id="adapter">adapter</summary>
290
+
291
+ Specifies the adapter to be used. The `adapter` method is the only one that does
292
+ not support lambda as it's value, as that would be the definition of undefined
293
+ behavior. If you want to support polymorphic collections, you should use the `type`
294
+ instead in combination with the `serializer`.
295
+
296
+ ```ruby
297
+ #without options
298
+ adapter SimpleAMS::Adapters::JSONAPI
299
+ #with adapter-specific options
300
+ adapter SimpleAMS::Adapters::JSONAPI, {root: false}
301
+ ```
302
+
303
+ Note that you can even specify your own adapter. Usually you will want to inherit
304
+ from an existing adapter (like `SimpleAMS::Adapters::AMS`), but that's not a
305
+ requirement. All you need is to duck type to 2 methods:
306
+ * `initialize(document, options = {})` be able to accept 2 arguments when your adapter
307
+ is instantiated. The first one is a document, while the second one is the adapter-specific
308
+ options (like the `{root: false}`.
309
+ * `as_json` method returns that returns the hash representation of the serialized result
310
+
311
+ The conversion of a Hash into raw JSON string is out of the scope of this library.
312
+ But you will probably want to use the fastest implementation possible like [oj](https://github.com/ohler55/oj).
313
+ </details>
314
+
315
+ <details>
316
+ <summary id="primary_id">primary_id</summary>
317
+
318
+ Specifies the `primary_id` to be used. There are many API specs that handle the
319
+ identifier of a resource in a different way than the rest of the attributes.
320
+ JSON:API is one of those.
321
+
322
+ ```ruby
323
+ #without options
324
+ primary_id :id
325
+ #with adapter-specific options
326
+ adapter :id, {external: true}
327
+ #dynamic
328
+ adapter ->(obj, s) { [obj.class.primary_key, {}]}
329
+ ```
330
+
331
+
332
+ </details>
333
+
334
+ <details>
335
+ <summary id="type">type</summary>
336
+
337
+ Specifies the `type` to be used. There are many API specs that handle the
338
+ type of a resource in a different way than the rest of the attributes.
339
+ JSON:API is one of those.
340
+
341
+ ```ruby
342
+ #without options
343
+ type :user
344
+ #with adapter-specific options
345
+ type :user, {polymorphic: false}
346
+ #dynamic
347
+ type ->(obj, s){ obj.employee? ? [:employee, {}] : [:user, {}]}
348
+ ```
349
+
350
+ </details>
351
+
352
+
353
+
354
+ #### name-value-hashmap type of directives
355
+ These are similar to the above, only that they also have an actual value, which
356
+ is converted to a representation through the adapter.
357
+ Such options are `link`, `meta`, `form` and the most generic directive `generic`.
358
+
359
+ For instance, think about a links. According to RFC [8288](https://tools.ietf.org/html/rfc8288#section-2), a link has
360
+
361
+ * a link context,
362
+ * a link relation type,
363
+ * a link target, and
364
+ * optionally, target attributes
365
+
366
+ Now, if we wanted to translate that to our serializers, a link could look like:
367
+ ```ruby
368
+ link :feed, '/api/v1/me/feed', {style: :compact}
369
+ ```
370
+
371
+ Here obviously the link context is the serializer itself, the link relation is
372
+ the feed, and the value is `/api/v1/me/feed`. Now you might say, feed should be
373
+ the name of the link which is different from the relation type.
374
+ The relation type could be `microposts`.
375
+ And actually, that’s the case for [JSONAPI v1.1](https://jsonapi.org/format/1.1/).
376
+ In that case, the feed should be treated barely as a name (whatever that means)
377
+ and relation type will be put inside the link options like:
378
+
379
+ ```ruby
380
+ link :feed, '/api/v1/me/feed', {rel: :microposts, style: :compact}
381
+ ```
382
+
383
+ Note however that this needs to be supported by the adapter you are using.
384
+
385
+ Similar to the case of value-hash directives, it is possible to have dynamic
386
+ value and options:
387
+
388
+ ```ruby
389
+ #values can be dynamic through lambdas
390
+ #lambdas take arguments the object to be serialized and the instantiated serializer
391
+ link :feed, ->(obj, s) { [s.api_v1_user_feed_path(user_id: obj.id), {rel: :feed} }
392
+ #if the value inside the lambda is single (no array), the options will be taken from
393
+ #the second argument, after the lambda. So the above is equivelent to:
394
+ link :feed, ->(obj, s) { s.api_v1_user_feed_path(user_id: obj.id) }, rel: :feed
395
+ ```
396
+
397
+ <details>
398
+ <summary id="link">link</summary>
399
+
400
+ Specifies a link to be used. There are many API specs that handle the links of a
401
+ resource in a special way (JSON:API is one of those).
402
+ You can specify multiple links, as long as each link name is unique.
403
+
404
+ ```ruby
405
+ #specifying a link with without options
406
+ link :feed, "/api/v1/feed"
407
+ #specifying a link with options
408
+ link :feed, "/api/v1/feed", {rel: :feed, compact: true}
409
+ #values can be dynamic through lambdas
410
+ #lambdas take arguments the object to be serialized and the instantiated serializer
411
+ link :feed, ->(obj, s) { [s.api_v1_user_feed_path(user_id: obj.id), {rel: :feed, compact: true}] }
412
+ #if the value inside the lambda is single (no array), the options will be taken from
413
+ #the second argument, after the lambda. So the above is equivelent to:
414
+ link :feed, ->(obj, s) { s.api_v1_user_feed_path(user_id: obj.id) }, rel: :feed, compact: true
415
+ ```
416
+
417
+ </details>
418
+
419
+ <details>
420
+ <summary id="meta">meta</summary>
421
+
422
+ Specifies a meta to be used. There are many API specs that handle the metas of a
423
+ resource in a special way (JSON:API is one of those).
424
+ You can specify multiple metas, as long as each link name is unique.
425
+
426
+ ```ruby
427
+ #specifying a meta with without options
428
+ meta :total_count, 1
429
+ #specifying a meta with options
430
+ meta :total_count, 1, {compact: true}
431
+ #values can be dynamic through lambdas
432
+ #lambdas take arguments the object to be serialized and the instantiated serializer
433
+ #in this case an object is apparently a collection/array
434
+ meta :total_count, ->(obj, s) { [obj.count, {compact: true}] }
435
+ #if the value inside the lambda is single (no array), the options will be taken from
436
+ #the second argument, after the lambda. So the above is equivelent to:
437
+ meta :total_count, ->(obj, s) { obj.count }, {compact: true}
438
+ ```
113
439
 
114
- # Advanced usage
115
- The DSL in the previous example is just syntactic sugar. In the basis, there is a very powerful
116
- hash-based DSL that can be used in 3 different places:
440
+ </details>
117
441
 
118
- * When initializing the `SimpleAMS::Renderer` class to render the data using specific serializer, adapter and options.
119
- * Inside a class that has the `SimpleAMS::DSL` included, using the `with_options({})` class method
120
- * Through the DSL, powered with some syntactic sugar
442
+ <details>
443
+ <summary id="form">form</summary>
121
444
 
122
- In any case, we have the following options:
445
+ Specifies a form to be used. Unfortunately, there are very few API specs that
446
+ handle forms (the [Ion hypermedia type](https://ionspec.org) is one of those).
447
+ You can specify multiple forms, as long as each link name is unique.
448
+
449
+ ```ruby
450
+ #specifying a form with without options
451
+ form :upload, {method: :get, url: "/api/v1/submit"}
452
+ #specifying a form with options
453
+ form :upload, {method: :get, url: "/api/v1/submit"}, compact: true
454
+ #values can be dynamic through lambdas
455
+ #lambdas take arguments the object to be serialized and the instantiated serializer
456
+ form :upload, ->(obj, s) { [obj.class.upload_form_options, {compact: true}] }
457
+ #if the value inside the lambda is single (no array), the options will be taken from
458
+ #the second argument, after the lambda. So the above is equivelent to:
459
+ form :upload, ->(obj, s) { obj.class.upload_form_options }, {compact: true}
460
+ ```
461
+
462
+ </details>
463
+
464
+ <details>
465
+ <summary id="generic">generic</summary>
466
+
467
+ Specifies a generic to be used. A generic is just a placeholder for extensions
468
+ that are unknown to SimpleAMS (but maybe they make a lot of sense to you ^_^)
469
+
470
+ ```ruby
471
+ #specifying a generic with without options
472
+ generic :pagination, :extended
473
+ #specifying a form with options
474
+ generic :pagination, :extended, compact: false
475
+ #values can be dynamic through lambdas
476
+ #lambdas take arguments the object to be serialized and the instantiated serializer
477
+ generic :pagination, ->(obj, s) { [obj.class.pagination_type, {compact: false}] }
478
+ #if the value inside the lambda is single (no array), the options will be taken from
479
+ #the second argument, after the lambda. So the above is equivelent to:
480
+ generic :pagination, ->(obj, s) { obj.class.pagination_type }, {compact: false}
481
+ ```
482
+
483
+ </details>
484
+
485
+ ##### group of links/metas/forms/generics
486
+
487
+ Each of the aforementioned options comes with a plural form as well.
488
+ For instance, if we want to specify multiple `links` at the same time:
489
+
490
+ ```ruby
491
+ links {
492
+ self: ['/api/v1/me', {rel: :user}]
493
+ feed: ['/api/v1/me/feed', {rel: :feed}]
494
+ }
495
+ ```
496
+
497
+ or if we want to specify multiple `metas`:
498
+ ```ruby
499
+ metas {
500
+ total_count: ->(obj){ obj.count }
501
+ pages: ->(obj){ obj.pages_count }
502
+ }
503
+ ```
504
+
505
+ Same goes for `forms` and `generics`.
506
+
507
+ #### collection directive
508
+ SimpleAMS has a unique ability to allow you specify different options when you are
509
+ rendering a collection. In its most simple use case it specified the plural name
510
+ of the resource, used when rendering a collection:
511
+
512
+ ```ruby
513
+ collection :users
514
+ ```
515
+
516
+ It’s needed, if your adapter serializes the collection using a root element.
517
+ But it can do much more than that: it allows you to define directives on the
518
+ collection level. For instance, if you want to have a link that should be
519
+ applied **only** to the collection level and not to each resource of the collection,
520
+ then you need to define it inside the collection’s block:
521
+
522
+ ```ruby
523
+ collection :users do
524
+ link :self, "/api/v1/users"
525
+ end
526
+ ```
527
+
528
+ Or if we also want to have the total count of the collection, that should go in
529
+ there actually:
530
+
531
+ ```ruby
532
+ collection :users do
533
+ link :self, "/api/v1/users"
534
+ meta :count, ->(collection, s) { collection.count }
535
+ end
536
+ ```
537
+
538
+ Again, inside that block you can define using the regular DSL, whatever you
539
+ would define in the resource level. It’s just yet another level of recursion
540
+ since, the same things that I show you here can be applied in the collection
541
+ level inside the block. For instance, in theory (and if the adapter supports
542
+ it), you can specify relations that apply only to the collection level:
543
+
544
+ ```ruby
545
+ class UserSerializer
546
+ include SimpleAMS::DSL
547
+
548
+ adapter SimpleAMS::Adapters::JSONAPI
549
+
550
+ attributes :id, :name, :email, :created_at, :role
551
+
552
+ type :user
553
+ collection :users do
554
+ link :self, "/api/v1/users"
555
+ meta :count, ->(collection, s) { collection.count }
556
+
557
+ has_one :s3_uploader #whatever that means :P
558
+ end
559
+
560
+ has_many :microposts
561
+ end
562
+ ```
563
+
564
+ ### Rendering DSL
565
+ When rendering a resource, it should be straightforward:
566
+
567
+ ```ruby
568
+ SimpleAMS::Renderer.new(user, { serializer: UserSerializer }).to_json
569
+ ```
570
+
571
+ All you need is to specify a serializer. In the example above, the resulted
572
+ resource is a reflection of what is defined inside the serializer.
573
+ However, the serializer acts as a filtering mechanism, meaning that you can
574
+ override anything the serializer defines, given that the result creates a
575
+ **subset and not a superset** (any superset options will be ignored).
576
+
577
+ For instance, you can override the type during rendering:
578
+
579
+ ```ruby
580
+ SimpleAMS::Renderer.new(user, {
581
+ serializer: UserSerializer, type: :person
582
+ }).to_json
583
+ ```
584
+ or you can override the relations, and specify that you don’t want to include
585
+ any relation defined in the serializer:
586
+
587
+ ```ruby
588
+ SimpleAMS::Renderer.new(user, {
589
+ serializer: UserSerializer, includes: []
590
+ }).to_json
591
+ ```
592
+
593
+ or specify exactly what fields you want:
594
+
595
+ ```ruby
596
+ SimpleAMS::Renderer.new(user, {
597
+ serializer: UserSerializer, fields: [:id, :email, :name, :created_at]
598
+ }).to_json
599
+ ```
600
+
601
+ or even specify the links subset that you want:
602
+
603
+ ```ruby
604
+ SimpleAMS::Renderer.new(user, {
605
+ serializer: UserSerializer, fields: [:id, :email, :name, :created_at],
606
+ links: [:self, :comments, :posts]
607
+ }).to_json
608
+ ```
609
+
610
+ and the list goes on.. basically the rendering DSL is identical
611
+
612
+ #### includes vs relations
613
+ There might be some confusion between `includes` and `relations`, so to clear things up:
614
+ * `includes`: specifies which relations you want to include, out of the available relations.
615
+ * `relations`: specifies the available relations, so it's not just an array of
616
+ symbols, but rather full relation objects which are generated through the dsl.
617
+ The raw representation of relations is an array of objects where each object
618
+ is `[relation_type, name, options, embedded_options]`. Here
619
+ `relation_type` is the type of the relation (`has_many`, `belongs_to` etc),
620
+ `name` is the name of the relation (like users), `options`, any relation options,
621
+ and `embedded_options` relevant to embedded options.
622
+
623
+ So when rendering, if you don't want any relations at all, the correct way is to
624
+ specify `includes: []`. In practice you can use `relations: []` as well, but that
625
+ will mean that the serializer has no relations at all (takes precedence over
626
+ `includes`). But that's not the correct way to do it. For instance, thing about
627
+ another scenario: you want to specify only one relation, the `feed` relation.
628
+ With `includes` you would have `includes: [:feed]`. With relations, you would
629
+ have to specify the relation at runtime (
630
+ `relations: [[:has_one, :feed, {serializer: FeedSerializer, fields: [:id, :content]}, {}]]`
631
+ ) and then also specify that you only want that: `includes: [:feed]`.
632
+
633
+ In general, there is no reason why you should use `relations` at rendering time,
634
+ instead you should leave that to the serializer, and only specify the `includes`.
635
+
636
+ Btw you might have noticed `includes` only as a rendering option, but SimpleAMS
637
+ DSL is used all over the place, and actually it's a serializer option as well
638
+ (just that it's not very useful ^_^).
639
+
640
+
641
+ #### Rendering collections
642
+ Rendering a collection is similar, only that you need to call
643
+ `SimpleAMS::Renderer::Collection` instead of just `SimpleAMS::Renderer`:
644
+
645
+ ```ruby
646
+ SimpleAMS::Renderer::Collection.new(users, {
647
+ serializer: UserSerializer, fields: [:id, :email, :name, :created_at],
648
+ links: [:self, :comments, :posts]
649
+ }).to_json
650
+ ```
651
+
652
+ Note that even with collection, by default everything goes to the resource.
653
+ If you need to specify options for the collection itself, you need to use the
654
+ collection key. For instance, having some metas inside the collection:
655
+
656
+ ```ruby
657
+ SimpleAMS::Renderer::Collection.new(users, {
658
+ serializer: UserSerializer, fields: [:id, :email, :name, :created_at],
659
+ links: [:self, :comments, :posts],
660
+ collection: {
661
+ metas: [:total_count]
662
+ }
663
+ }).to_json
664
+ ```
665
+
666
+ #### Rendering options with values
667
+ If you want to specify the actual values when rendering the resource, rather
668
+ than taking into account the serializer, you can inject a hashmap:
669
+
670
+ ```ruby
671
+ SimpleAMS::Renderer::Collection.new(users, {
672
+ serializer: UserSerializer, fields: [:id, :email, :name, :created_at],
673
+ links: [:self, :comments, :posts],
674
+ collection: {
675
+ metas: {
676
+ total_count: users.count,
677
+ }
678
+ }).to_json
679
+ ```
680
+
681
+ Of course, you can also pass a lambda there, but not sure what’s the point since
682
+ the lambda parameter is the resource that you already try to render so it’s not
683
+ going to give you anything more (and will be slower actually).
684
+
685
+ #### Exposing methods inside the serializer, like helpers
686
+ When rendering you can expose a couple of objects in the serializer:
687
+
688
+ ```ruby
689
+ SimpleAMS::Renderer::Collection.new(users, {
690
+ serializer: UserSerializer, fields: [:id, :email, :name, :created_at],
691
+ #exposing helpers that will be available inside the serializer
692
+ expose: {
693
+ #a class
694
+ current_user: User.first
695
+ #or a module
696
+ helpers: CommonHelpers
697
+ },
698
+ }).to_json
699
+ ```
700
+
701
+ The expose attribute is also available through DSL, although usually that’s not
702
+ very useful. Just wanted to mentions that there is actually parity on everything,
703
+ since everything has been built on the same building blocks :)
704
+
705
+ ### Extended DSL show off
706
+ Here is an extended example of the DSL. It's not a real use case of course, but
707
+ shows what's possible with SimpleAMS and its powerful DSL.
123
708
 
124
709
  ```ruby
125
710
  {
@@ -133,13 +718,14 @@ In any case, we have the following options:
133
718
  fields: [:id, :name, posts: [:id, :text], videos: [:id, :title, comments: [:id, :text]]] #overrides includes when association is specified
134
719
  relations: [
135
720
  [:belongs_to, :company, {
136
- serializer: CompanySerializer,
137
- fields: Company.column_names.map(&:to_sym)
721
+ serializer: CompanySerializer,
722
+ fields: Company.column_names.map(&:to_sym)
138
723
  }
139
724
  ],
140
725
  [:has_many, :followers, {
141
- serializer: UserSerializer,
142
- fields: User.column_names.map(&:to_sym)
726
+ serializer: UserSerializer,
727
+ fields: User.column_names.map(&:to_sym)
728
+ }
143
729
  ],
144
730
  ]
145
731
  #the serializer that should be used
@@ -199,45 +785,6 @@ In any case, we have the following options:
199
785
  }
200
786
  ```
201
787
 
202
- Now let those options be `OPTIONS`. These can be fed to either the `SimpleAMS::Renderer`
203
- or to the serializer class itself using the `with_options` class method. Let's see how:
204
-
205
- ```ruby
206
- class UserSerializer
207
- include SimpleAMS::DSL
208
-
209
- with_options({ #you can pass the same options as above ;)
210
- primary_id: :id,
211
- # ...
212
- # ...
213
- # ...
214
- })
215
-
216
- def name
217
- "#{object.first_name} #{object.last_name}"
218
- end
219
-
220
- def videos
221
- Videos.where(user_id: object.id).published
222
- end
223
- end
224
- ```
225
-
226
- The same options can be passed when calling the `Renderer`. `Renderer` can override
227
- some properties, however in all properties that act as sets/arrays (like
228
- attributes/fields, includes, links etc.), **specified serializer options take precedence** over
229
- `Renderer` options.
230
-
231
- ```ruby
232
- SimpleAMS::Renderer.new(user, {
233
- primary_id: :id,
234
- serializer: UserSerializer,
235
- # ...
236
- # ...
237
- # ...
238
- }).to_json
239
- ```
240
-
241
788
  ## Development
242
789
 
243
790
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.