simple_ams 0.2.5 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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.