shreddies 0.1.0 → 0.5.1

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.
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