shreddies 0.1.0 → 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6770e333d8c5c90f6a7f1d56fd4d3e27fa32103c13473ed500d151ee20cd8826
4
- data.tar.gz: 387cc3e15bbaa1eb8ca3c1775fbc80d22df8c7edf79547dd1518149f07d70e68
3
+ metadata.gz: d6daee669edb85f0a632236f2eacdec946b07d9419a25ae88f6beb6bd09c4a19
4
+ data.tar.gz: f673da386a57e68668a356c8687a88a5f0ae7ec934ebab439b48d5fd2d53ef64
5
5
  SHA512:
6
- metadata.gz: 8a7ef85a2a7e0cf2bac9353ce5998f3b7b897c36da844e7169e13dfea62c377cf7a994b4e0bf27a763721a3853aa503303ff2248b65a6ade68b9db4748b027ec
7
- data.tar.gz: c2b54d864565890b793b63bdbdf6160d85ff7dd3a8de974034499139aedb7c2fb0974cb34ae05acda31d7d444caa480522f4c9408c6284709a6a1168f552f221
6
+ metadata.gz: ec6d5dc108f25b5d4f820574ae8051e5cc594b24d101de65f8145f476bf6f782db0361c4ffdacf6505b7fbb96708e5b6cd4ed7ff704ad4e3bd2d5653a86e3e49
7
+ data.tar.gz: 4c330bf894abf811866bd21ad77460cd7073ad89d5e97719bc2e2af73bf2d1255808b3487e6880d22c52f5e8a130a19d3b52b09c4ecd711abb4dcc71f52a46ca
data/.gitignore CHANGED
@@ -6,3 +6,4 @@
6
6
  /pkg/
7
7
  /spec/reports/
8
8
  /tmp/
9
+ /test/internal/db/*.sqlite
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- shreddies (0.1.0)
4
+ shreddies (0.5.0)
5
5
  activerecord (>= 5)
6
6
  railties (>= 5)
7
7
 
data/README.md CHANGED
@@ -2,6 +2,8 @@
2
2
 
3
3
  Shreddies is a JSON serialization library for Rails that focuses on simplicity and speed. No more "magic" DSL's - just plain old Ruby objects! It's primarily intended to serialize Rails models as JSON, but will also work with pretty much anything at all.
4
4
 
5
+ Shreddies primary principle is to be explicit. So a serializer will return nothing until you define some methods. This gives you complete control and everything is a known quantity - no surprises.
6
+
5
7
  ## Installation
6
8
 
7
9
  Add this line to your application's Gemfile:
@@ -20,7 +22,7 @@ Or install it yourself as:
20
22
 
21
23
  ## Usage
22
24
 
23
- Serializers should be named after your models and located in "`app/serializers`". Any public methods you define will be serialized, and you can quickly expose methods from the serialized subject using the `delegate` class method.
25
+ Serializers should be named after your models and located in "`app/serializers`". Any public methods you define will be serialized, and you can quickly expose methods from the serialized subject using the `delegate` class method, which simply delegates to the subject.
24
26
 
25
27
  ```ruby
26
28
  # app/serializers/user_serializer.rb
@@ -65,12 +67,122 @@ Model collections and array's are also supported:
65
67
  User.all.as_json
66
68
  ```
67
69
 
70
+ ### Collection and Single Modules
71
+
72
+ You may find that you don't want or need to return as much data in collections of objects, or may want to include differtent data. So if a serializer defines a `Collection` module, and a collection or array is being rendered, then that Collection module will automatically be included:
73
+
74
+ ```ruby
75
+ ArticleSerializer < Shreddies::Json
76
+ module Collection
77
+ def url
78
+ "https://blah.com/#{subject.slug}"
79
+ end
80
+ end
81
+ end
82
+ ```
83
+
84
+ Conversely, you can define a `Single` module, and that will be included when rendering a single object.
85
+
86
+ ```ruby
87
+ ArticleSerializer < Shreddies::Json
88
+ module Single
89
+ def body
90
+ 'this body is really, really long, and I do not want it returned in lists.'
91
+ end
92
+ end
93
+ end
94
+ ```
95
+
96
+ ### ActiveRecord Associations
97
+
98
+ ActiveRecord associations are supported with no additional work on your part. Shreddies will simply call `#as_json` on any method that returns an ActiveRecord model or relation.
99
+
100
+ ```ruby
101
+ # app/serializers/user_serializer.rb
102
+ class UserSerializer < Shreddies::Json
103
+ delegate :articles
104
+
105
+ def latest_article
106
+ articles.latest
107
+ end
108
+ end
109
+ ```
110
+
111
+ And if you need to be specific about what you render, just call the serializer or `#as_json` directly:
112
+
113
+ ```ruby
114
+ # app/serializers/user_serializer.rb
115
+ class UserSerializer < Shreddies::Json
116
+ def articles
117
+ subject.articles.as_json index_by: :slug
118
+ end
119
+
120
+ def latest_article
121
+ LatestArticleSerializer.render articles.latest
122
+ end
123
+ end
124
+ ```
125
+
126
+ ### `before_render` callback
127
+
128
+ You can define a `#before_render` private method in your serializers, which will act as a callback. It receives the object to be output, and expects you to return the object, which allows you to modify it before rendering.
129
+
68
130
  ### Options
69
131
 
70
132
  Both `#as_json` and `.render` accepts an `options` hash, which will be forwarded to the serializer class, and available as `options`. This allows you to pass arbitrary options and use them in your serializer.
71
133
 
72
134
  The following standard options are supported, and provide additional built-in functionality:
73
135
 
136
+ #### `serializer`
137
+
138
+ By default `#as_json` will look for a serializer named after your model. So a `User` model will automatically use the `UserSerializer`. Sometimes you want to use a different serializer class, in which case you can use the `serializer` option:
139
+
140
+ ```ruby
141
+ User.all.as_json serializer: User::AdminSerializer
142
+ ```
143
+
144
+ #### `module`
145
+
146
+ You can pass one or module names in the `module` option, and these modules will be included into the serializer. This is great for selectively including attributes and methods.
147
+
148
+ ```ruby
149
+ Article.all.as_json module: :WithBody
150
+ ```
151
+
152
+ ```ruby
153
+ ArticleSerializer < Shreddies::Json
154
+ module WithBody
155
+ def body
156
+ 'This article body is really, really long'
157
+ end
158
+ end
159
+ end
160
+ ```
161
+
162
+ The `Collection` and `Single` modules can be defined and they will be automatically included. The Collection module will be included when rendering an array or ActiveRecord collection (`ActiveRecord::Relation`), and the Single module will be included when rendering a single obejct.
163
+
164
+ #### `transform_keys` (default: true)
165
+
166
+ If false, the returned keys will not be transformed. The default is to deeply transform all keys to camelCase.
167
+
168
+ #### `except`
169
+
170
+ Pass one or more attribute names as a Symbol or Array of Symbols, and these will be excluded from the results:
171
+
172
+ ```ruby
173
+ User.all.as_json(except: [:first_name, :age])
174
+ ```
175
+
176
+ #### `only`
177
+
178
+ Pass one or more attribute names as a Symbol or Array of Symbols, and _ONLY_ these will be included in the results:
179
+
180
+ ```ruby
181
+ User.all.as_json(only: :first_name)
182
+ ```
183
+
184
+ > Attributes must still be defined within the Serializer.
185
+
74
186
  #### `index_by`
75
187
 
76
188
  Give this option a property of your serialized subject as a Symbol, and the returned collection will be a Hash keyed by that property.
@@ -99,6 +211,33 @@ User.all.as_json index_by: :id
99
211
  }
100
212
  ```
101
213
 
214
+ ### Serializer Inheritance
215
+
216
+ A serializer can inherit from any other serializer, which is a great way to create custom views:
217
+
218
+ ```ruby
219
+ # app/serializers/user_serializer.rb
220
+ class UserSerializer < Shreddies::Json
221
+ delegate :id, :first_name, :last_name, :email
222
+
223
+ def name
224
+ "#{first_name} #{last_name}"
225
+ end
226
+ end
227
+
228
+ class User::AdministratorSerializer < UserSerializer
229
+ def type
230
+ 'administrator'
231
+ end
232
+ end
233
+ ```
234
+
235
+ Then call it like any other serializer:
236
+
237
+ ```ruby
238
+ User::AdministratorSerializer.render(user)
239
+ ```
240
+
102
241
  ## Development
103
242
 
104
243
  After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
@@ -7,12 +7,10 @@ module Shreddies
7
7
  serializer = options.delete(:serializer) || "#{model_name}Serializer"
8
8
 
9
9
  if serializer.is_a?(String) || serializer.is_a?(Symbol)
10
- serializer.to_s.constantize.render_as_json self, options
11
- else
12
- serializer.render self, options
10
+ serializer = serializer.to_s.safe_constantize
13
11
  end
14
- rescue NameError
15
- super
12
+
13
+ serializer ? serializer.render_as_json(self, options) : super
16
14
  end
17
15
  end
18
16
 
@@ -21,12 +19,10 @@ module Shreddies
21
19
  serializer = options.delete(:serializer) || "#{model_name}Serializer"
22
20
 
23
21
  if serializer.is_a?(String) || serializer.is_a?(Symbol)
24
- serializer.to_s.constantize.render_as_json self, options
25
- else
26
- serializer.render self, options
22
+ serializer = serializer.to_s.safe_constantize
27
23
  end
28
- rescue NameError
29
- super
24
+
25
+ serializer ? serializer.render_as_json(self, options) : super
30
26
  end
31
27
  end
32
28
  end
@@ -6,20 +6,28 @@ module Shreddies
6
6
  # Render a subject as json, where `subject` is a single object (usually a Rails model), or an
7
7
  # array/collection of objects.
8
8
  #
9
+ # If subject is an array/collection then it will look for a `Collection` module prepend it to
10
+ # the `module` option.
11
+ #
9
12
  # A Hash of options can be given as the second argument:
10
13
  # - index_by - Key the returned array by the value, transforming it from an array to a hash.
14
+ # - module - A Symbol or String of a local module to include. Or an array of several
15
+ # modules, where each will be mixed in in order. Use this to mix in groups of
16
+ # attributes. Eg. `ArticleSerializer.render(data, module: :WithBody)`.
11
17
  #
12
18
  def render(subject, options = {})
13
19
  index_by = options.delete(:index_by)
14
20
 
15
21
  if subject.is_a?(Array) || subject.is_a?(ActiveRecord::Relation)
22
+ collection_options = options.merge(from_collection: true)
23
+
16
24
  if index_by
17
25
  mapped = {}
18
26
  subject.each do |x|
19
- mapped[x[index_by]] = new(x, options)
27
+ mapped[x[index_by]] = new(x, collection_options)
20
28
  end
21
29
  else
22
- mapped = subject.map { |x| new(x, options) }
30
+ mapped = subject.map { |x| new(x, collection_options) }
23
31
  end
24
32
 
25
33
  mapped.as_json
@@ -31,31 +39,81 @@ module Shreddies
31
39
  alias render_as_json render
32
40
  end
33
41
 
34
- # Monkey patches Rails Module#delegate so that the `:to` argument defaults tio `:subject`.
42
+ # Monkey patches Rails Module#delegate so that the `:to` argument defaults to `:subject`.
35
43
  def self.delegate(*methods, to: :subject, prefix: nil, allow_nil: nil, private: nil)
36
44
  super(*methods, to: to, prefix: prefix, allow_nil: allow_nil, private: private)
37
45
  end
38
46
 
39
47
  attr_reader :subject, :options
40
48
 
41
- def initialize(subject, options)
42
- @subject = subject
43
- @options = options
49
+ def initialize(subject, opts = {})
50
+ @subject = subject.is_a?(Hash) ? OpenStruct.new(subject) : subject
51
+ @options = { transform_keys: true }.merge(opts)
52
+
53
+ extend_with_modules
44
54
  end
45
55
 
56
+ # Travel through the ancestors that are serializers (class name ends with "Serializer"), and
57
+ # call all public instance methods, returning a hash.
46
58
  def as_json
47
- json = {}
59
+ output = {}.with_indifferent_access
60
+ methods = Set.new(public_methods(false))
61
+
62
+ self.class.ancestors.each do |ancestor|
63
+ if ancestor.to_s.end_with?('Serializer')
64
+ methods.merge ancestor.public_instance_methods(false)
65
+ end
66
+ end
48
67
 
49
- methods = public_methods(false)
50
- if self.class.superclass.to_s.end_with?('Serializer')
51
- methods.concat self.class.superclass.public_instance_methods(false)
68
+ # Filter out methods using the `only` or `except` options.
69
+ if @options[:only]
70
+ @options[:only] = Array(@options[:only])
71
+ methods = methods.select { |x| @options[:only].include? x }
72
+ elsif @options[:except]
73
+ methods = methods.excluding(@options[:except])
52
74
  end
53
75
 
54
- methods.uniq.excluding(:subject, :options, :as_json).map do |attr|
55
- json[attr] = public_send(attr)
76
+ methods.map do |attr|
77
+ res = public_send(attr)
78
+ if res.is_a?(ActiveRecord::Relation) || res.is_a?(ActiveRecord::Base)
79
+ res = res.as_json(transform_keys: @options[:transform_keys])
80
+ end
81
+
82
+ output[attr] = res
83
+ end
84
+
85
+ output = before_render(output)
86
+
87
+ return output unless @options[:transform_keys]
88
+
89
+ output.deep_transform_keys { |key| key.to_s.camelize :lower }
90
+ end
91
+
92
+ private
93
+
94
+ def before_render(output)
95
+ output
96
+ end
97
+
98
+ def extend_with_modules
99
+ self.class.ancestors.reverse.each do |ancestor|
100
+ next unless ancestor.to_s.end_with?('Serializer')
101
+
102
+ # Extend with Collection module if it exists, and a collection is being rendered. Otherwise,
103
+ # extend with the Single module if that exists.
104
+ if @options[:from_collection]
105
+ (collection_mod = "#{ancestor}::Collection".safe_constantize) && extend(collection_mod)
106
+ else
107
+ (single_mod = "#{ancestor}::Single".safe_constantize) && extend(single_mod)
108
+ end
56
109
  end
57
110
 
58
- json.deep_transform_keys { |key| key.to_s.camelize :lower }
111
+ # Extend with the :module option if given.
112
+ if @options[:module]
113
+ Array(@options[:module]).each do |m|
114
+ extend m.is_a?(Module) ? m : "#{self.class}::#{m}".constantize
115
+ end
116
+ end
59
117
  end
60
118
  end
61
119
  end
@@ -1,3 +1,3 @@
1
1
  module Shreddies
2
- VERSION = "0.1.0"
2
+ VERSION = "0.5.1"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shreddies
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Joel Moss
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-06-30 00:00:00.000000000 Z
11
+ date: 2020-07-28 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activerecord