transmutation 0.2.2 → 0.3.0

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: 3bf7696ac47e240fb64569d269e95f1cb2d369b34397a84c356469ab8975b4b5
4
- data.tar.gz: 1b8a064d00de95904fa5aabbc49598cd18a5fa8432b35494f050ec5d1f24508a
3
+ metadata.gz: a1184809d63cb5abaad091e433a5e150579f5ca634a5522c57b7dd49c3465a3c
4
+ data.tar.gz: a0e2bddde883737a82369d14cd4bc75235ea1dee366c7a0234a262e29930a74c
5
5
  SHA512:
6
- metadata.gz: 8f2590ad630f228024acd3b6fa229ef4fb61c2b0abf590098aeda6ebbd4d52b3f74d4fbba398977594998873dc3f758865c176bf0de947c353478b74666647d4
7
- data.tar.gz: 7d1e9ff02220ec546e1226dbdd201960585668fcc4f03e702c484fd19273226ec90e550bde8ce877ffad1724f7167121db9b5902ba440b5ad96c5de42ffd2de8
6
+ metadata.gz: ee23262517dba8436c182542b02bd3e1b15df0225b84eae7dedb106c4d155cf6dd78db791e9a98d44c57654dcf87d9aa8110e87cde6bface0943952199f8e2a5
7
+ data.tar.gz: 8b23bfbd305587fde6d6fef4a714840a73e4415d27902c3169dd06561f141b127944d64f13029a37f9aaf10d88bdab5b1059554ae132fc4584c899f6ecb3148e
data/.rubocop.yml CHANGED
@@ -16,4 +16,8 @@ Style/StringLiteralsInInterpolation:
16
16
  Layout/LineLength:
17
17
  Max: 120
18
18
 
19
+ Metrics/ParameterLists:
20
+ Enabled: true
21
+ CountKeywordArgs: false
22
+
19
23
  require: rubocop-rspec
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- transmutation (0.2.2)
4
+ transmutation (0.3.0)
5
5
  zeitwerk (~> 2.6.15)
6
6
 
7
7
  GEM
data/README.md CHANGED
@@ -2,19 +2,29 @@
2
2
 
3
3
  Transmutation is a Ruby gem that provides a simple way to serialize Ruby objects into JSON.
4
4
 
5
- It takes inspiration from the [Active Model Serializers](https://github.com/rails-api/active_model_serializers) gem, but strips away adapters.
5
+ It also adds an opinionated way to automatically find and use serializer classes based on the object's class name and the caller's namespace - it takes inspiration from the [Active Model Serializers](https://github.com/rails-api/active_model_serializers) gem, but strips away adapters.
6
6
 
7
7
  It aims to be a performant and elegant solution for serializing Ruby objects into JSON, with a touch of opinionated "magic" :sparkles:.
8
8
 
9
9
  ## Installation
10
10
 
11
- Install the gem and add to the application's Gemfile by executing:
11
+ Install the gem and add to your application's Gemfile by executing:
12
12
 
13
- $ bundle add transmutation
13
+ ```bash
14
+ bundle add transmutation
15
+ ```
16
+
17
+ or manually add the following to your Gemfile:
18
+
19
+ ```ruby
20
+ gem "transmutation"
21
+ ```
14
22
 
15
23
  If bundler is not being used to manage dependencies, install the gem by executing:
16
24
 
17
- $ gem install transmutation
25
+ ```bash
26
+ gem install transmutation
27
+ ```
18
28
 
19
29
  ## Usage
20
30
 
@@ -48,13 +58,16 @@ If bundler is not being used to manage dependencies, install the gem by executin
48
58
 
49
59
  As long as your object responds to the attributes defined in the serializer, it can be serialized.
50
60
 
51
- - Struct
61
+ <details>
62
+ <summary>Struct</summary>
52
63
 
53
64
  ```ruby
54
65
  User = Struct.new(:id, :name, :email)
55
66
  ```
67
+ </details>
56
68
 
57
- - Class
69
+ <details>
70
+ <summary>Class</summary>
58
71
 
59
72
  ```ruby
60
73
  class User
@@ -67,8 +80,10 @@ If bundler is not being used to manage dependencies, install the gem by executin
67
80
  end
68
81
  end
69
82
  ```
83
+ </details>
70
84
 
71
- - ActiveRecord
85
+ <details>
86
+ <summary>ActiveRecord</summary>
72
87
 
73
88
  ```ruby
74
89
  # == Schema Information
@@ -81,15 +96,142 @@ If bundler is not being used to manage dependencies, install the gem by executin
81
96
  class User < ApplicationRecord
82
97
  end
83
98
  ```
99
+ </details>
100
+
101
+ ### The `#serialize` method
102
+
103
+ When you include the `Transmutation::Serialization` module in your class, you can use the `#serialize` method to serialize an object.
104
+
105
+ It will attempt to find a serializer class based on the object's class name along with the caller's namespace.
106
+
107
+ ```ruby
108
+ include Transmutation::Serialization
109
+
110
+ serialize(User.new) # => UserSerializer.new(User.new)
111
+ ```
112
+
113
+ If no serializer class is found, it will return the object as is.
114
+
115
+ ### With Ruby on Rails
116
+
117
+ When then `Transmutation::Serialization` module is included in a Rails controller, it also extends your `render` calls.
118
+
119
+ ```ruby
120
+ class Api::V1::UsersController < ApplicationController
121
+ include Transmutation::Serialization
122
+
123
+ def show
124
+ user = User.find(params[:id])
84
125
 
85
- ### Using the `Transmutation::Serialization` module
126
+ render json: user
127
+ end
128
+ end
129
+ ```
130
+
131
+ This will attempt to bubble up the controller namespaces to find a defined serializer class:
132
+
133
+ - `Api::V1::UserSerializer`
134
+ - `Api::UserSerializer`
135
+ - `UserSerializer`
136
+
137
+ This calls the `#serialize` method under the hood.
138
+
139
+ If no serializer class is found, it will fall back to the default behavior of rendering the object as JSON.
140
+
141
+ You can disable this behaviour by passing `serialize: false` to the `render` method.
142
+
143
+ ```ruby
144
+ render json: user, serialize: false # => user.to_json
145
+ ```
146
+
147
+ ## Configuration
148
+
149
+ You can override the serialization lookup by passing the following options:
150
+
151
+ - `namespace`: The namespace to use when looking up the serializer class.
152
+
153
+ ```ruby
154
+ render json: user, namespace: "V1" # => Api::V1::V1::UserSerializer
155
+ ```
156
+
157
+ To prevent caller namespaces from being appended to the provided namespace, prefix the namespace with `::`.
158
+
159
+ ```ruby
160
+ render json: user, namespace: "::V1" # => V1::UserSerializer
161
+ ```
162
+
163
+ The `namespace` key is forwarded to the `#serialize` method.
164
+
165
+ ```ruby
166
+ render json: user, namespace: "V1" # => serialize(user, namespace: "V1")
167
+ ```
168
+
169
+ - `serializer`: The serializer class to use.
170
+
171
+ ```ruby
172
+ render json: user, serializer: "SuperUserSerializer" # => Api::V1::SuperUserSerializer
173
+ ```
174
+
175
+ To prevent all namespaces from being appended to the serializer class, prefix the serializer class with `::`.
176
+
177
+ ```ruby
178
+ render json: user, serializer: "::SuperUserSerializer" # => SuperUserSerializer
179
+ ```
180
+
181
+ The `serializer` key is forwarded to the `#serialize` method.
182
+
183
+ ```ruby
184
+ render json: user, serializer: "SuperUserSerializer" # => serialize(user, serializer: "SuperUserSerializer")
185
+ ```
186
+
187
+ ## Opinionated Architecture
188
+
189
+ If you follow the pattern outlined below, you can take full advantage of the automatic serializer lookup.
190
+
191
+ ### File Structure
192
+
193
+ ```
194
+ .
195
+ └── app/
196
+ ├── controllers/
197
+ │ └── api/
198
+ │ ├── v1/
199
+ │ │ └── users_controller.rb
200
+ │ └── v2
201
+ │ └── users_controller.rb
202
+ ├── models/
203
+ │ └── user.rb
204
+ └── serializers/
205
+ └── api/
206
+ ├── v1/
207
+ │ └── user_serializer.rb
208
+ ├── v2/
209
+ │ └── user_serializer.rb
210
+ └── user_serializer.rb
211
+ ```
212
+
213
+ ### Serializers
214
+
215
+ ```ruby
216
+ class Api::UserSerializer < Transmutation::Serializer
217
+ attributes :id, :name, :email
218
+ end
219
+
220
+ class Api::V1::UserSerializer < Api::UserSerializer
221
+ attributes :phone # Added in V1
222
+ end
223
+
224
+ class Api::V2::UserSerializer < Api::UserSerializer
225
+ attributes :avatar # Added in V2
226
+ end
227
+ ```
228
+
229
+ To remove attributes, it is recommended to redefine all attributes and start anew. This acts as a reset and makes serializer inheritance much easier to follow.
86
230
 
87
231
  ## Development
88
232
 
89
233
  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.
90
234
 
91
- To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
92
-
93
235
  ## Contributing
94
236
 
95
237
  Bug reports and pull requests are welcome on GitHub at https://github.com/spellbook-technology/transmutation. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/spellbook-technology/transmutation/blob/main/CODE_OF_CONDUCT.md).
@@ -12,9 +12,9 @@ module Transmutation
12
12
  # Bubbles up the namespace until we find a matching serializer.
13
13
  #
14
14
  # @see Transmutation::Serialization#lookup_serializer
15
+ # @note This never bubbles up the object's namespace, only the caller's namespace.
15
16
  #
16
- # Example:
17
- #
17
+ # @example
18
18
  # namespace: Api::V1::Admin::Detailed
19
19
  # serializer: Chat::User
20
20
  #
@@ -25,11 +25,7 @@ module Transmutation
25
25
  # - Api::V1::Chat::UserSerializer
26
26
  # - Api::Chat::UserSerializer
27
27
  # - Chat::UserSerializer
28
- #
29
- # Note: This never bubbles up the object's namespace, only the caller's namespace.
30
28
  def serializer_for(object, serializer: nil)
31
- return Transmutation::CollectionSerializer if object.respond_to?(:map)
32
-
33
29
  serializer_name = serializer_name_for(object, serializer: serializer)
34
30
 
35
31
  return constantize_serializer!(Object, serializer_name, object: object) if serializer_name.start_with?("::")
@@ -38,7 +34,7 @@ module Transmutation
38
34
  return potential_namespace.const_get(serializer_name) if potential_namespace.const_defined?(serializer_name)
39
35
  end
40
36
 
41
- raise SerializerNotFound.new(@object, namespace: serializer_namespace, name: serializer_name)
37
+ raise SerializerNotFound.new(object, namespace: serializer_namespace, name: serializer_name)
42
38
  end
43
39
 
44
40
  # Returns the highest specificity serializer name for the given object.
@@ -47,8 +43,6 @@ module Transmutation
47
43
  #
48
44
  # @return [String] The serializer name.
49
45
  def serializer_name_for(object, serializer: nil)
50
- return "::Transmutation::CollectionSerializer" if object.respond_to?(:map)
51
-
52
46
  "#{serializer&.delete_suffix("Serializer") || object.class.name}Serializer"
53
47
  end
54
48
 
@@ -3,11 +3,11 @@
3
3
  module Transmutation
4
4
  module Serialization
5
5
  module Rendering
6
- def render(json: nil, serialize: true, **args)
6
+ def render(json: nil, serialize: true, namespace: nil, serializer: nil, max_depth: 1, **args)
7
7
  return super(**args) unless json
8
- return super(json: json, **args) unless serialize
8
+ return super(**args, json: json) unless serialize
9
9
 
10
- super(**args, json: serialize(json))
10
+ super(**args, json: serialize(json, namespace: namespace, serializer: serializer, max_depth: max_depth))
11
11
  end
12
12
  end
13
13
  end
@@ -8,10 +8,18 @@ module Transmutation
8
8
  # @param object [Object] The object to serialize.
9
9
  # @param namespace [String, Symbol, Module] The namespace to lookup the serializer in.
10
10
  # @param serializer [String, Symbol, Class] The serializer to use.
11
+ # @param max_depth [Integer] The maximum depth of nested associations to serialize.
11
12
  #
12
13
  # @return [Transmutation::Serializer] The serialized object. This will respond to `#as_json` and `#to_json`.
13
- def serialize(object, namespace: nil, serializer: nil)
14
- lookup_serializer(object, namespace: namespace, serializer: serializer).new(object)
14
+ def serialize(object, namespace: nil, serializer: nil, depth: 0, max_depth: 1)
15
+ if object.respond_to?(:map)
16
+ return object.map do |item|
17
+ serialize(item, namespace: namespace, serializer: serializer, depth: depth, max_depth: max_depth)
18
+ end
19
+ end
20
+
21
+ lookup_serializer(object, namespace: namespace, serializer: serializer)
22
+ .new(object, depth: depth, max_depth: max_depth)
15
23
  end
16
24
 
17
25
  # Lookup the serializer for the given object.
@@ -29,7 +37,7 @@ module Transmutation
29
37
  end
30
38
 
31
39
  private_class_method def self.included(base)
32
- base.include(Rendering) if base.respond_to?(:render)
40
+ base.include(Rendering) if base.method_defined?(:render)
33
41
  end
34
42
  end
35
43
  end
@@ -10,12 +10,20 @@ module Transmutation
10
10
  # attribute :full_name do
11
11
  # "#{object.first_name} #{object.last_name}".strip
12
12
  # end
13
+ #
14
+ # belongs_to :organization
15
+ #
16
+ # has_many :posts
13
17
  # end
14
18
  class Serializer
15
19
  extend ClassAttributes
16
20
 
17
- def initialize(object)
21
+ include Transmutation::Serialization
22
+
23
+ def initialize(object, depth: 0, max_depth: 1)
18
24
  @object = object
25
+ @depth = depth
26
+ @max_depth = max_depth
19
27
  end
20
28
 
21
29
  def to_json(options = {})
@@ -24,39 +32,84 @@ module Transmutation
24
32
 
25
33
  def as_json(_options = {})
26
34
  attributes_config.each_with_object({}) do |(attr_name, attr_options), hash|
27
- hash[attr_name.to_s] = attr_options[:block] ? instance_exec(&attr_options[:block]) : object.send(attr_name)
35
+ if attr_options[:association]
36
+ hash[attr_name.to_s] = instance_exec(&attr_options[:block]) if @depth + 1 <= @max_depth
37
+ else
38
+ hash[attr_name.to_s] = attr_options[:block] ? instance_exec(&attr_options[:block]) : object.send(attr_name)
39
+ end
28
40
  end
29
41
  end
30
42
 
31
- # Define an attribute to be serialized
32
- #
33
- # @param attribute_name [Symbol] The name of the attribute to serialize
34
- # @param block [Proc] The block to call to get the value of the attribute.
35
- # The block is called in the context of the serializer instance.
36
- #
37
- # @example
38
- # class UserSerializer < Transmutation::Serializer
39
- # attribute :first_name
40
- #
41
- # attribute :full_name do
42
- # "#{object.first_name} #{object.last_name}".strip
43
- # end
44
- # end
45
- def self.attribute(attribute_name, &block)
46
- attributes_config[attribute_name] = { block: block }
47
- end
43
+ class << self
44
+ # Define an attribute to be serialized
45
+ #
46
+ # @param attribute_name [Symbol] The name of the attribute to serialize
47
+ # @param block [Proc] The block to call to get the value of the attribute
48
+ # - The block is called in the context of the serializer instance
49
+ #
50
+ # @example
51
+ # class UserSerializer < Transmutation::Serializer
52
+ # attribute :first_name
53
+ #
54
+ # attribute :full_name do
55
+ # "#{object.first_name} #{object.last_name}".strip
56
+ # end
57
+ # end
58
+ def attribute(attribute_name, &block)
59
+ attributes_config[attribute_name] = { block: block }
60
+ end
61
+
62
+ # Define an association to be serialized
63
+ #
64
+ # @note By default, the serializer for the association is looked up in the same namespace as the serializer
65
+ #
66
+ # @param association_name [Symbol] The name of the association to serialize
67
+ # @param namespace [String, Symbol, Module] The namespace to lookup the association's serializer in
68
+ # @param serializer [String, Symbol, Class] The serializer to use for the association's serialization
69
+ #
70
+ # @example
71
+ # class UserSerializer < Transmutation::Serializer
72
+ # association :posts
73
+ # association :comments, namespace: "Nested", serializer: "User::CommentSerializer"
74
+ # end
75
+ def association(association_name, namespace: nil, serializer: nil)
76
+ block = lambda do
77
+ serialize(object.send(association_name), namespace: namespace, serializer: serializer, depth: @depth + 1)
78
+ end
79
+
80
+ attributes_config[association_name] = { block: block, association: true }
81
+ end
82
+
83
+ alias belongs_to association
84
+ alias has_one association
85
+ alias has_many association
86
+
87
+ # Shorthand for defining multiple attributes
88
+ #
89
+ # @param attribute_names [Array<Symbol>] The names of the attributes to serialize
90
+ #
91
+ # @example
92
+ # class UserSerializer < Transmutation::Serializer
93
+ # attributes :first_name, :last_name
94
+ # end
95
+ def attributes(*attribute_names)
96
+ attribute_names.each do |attribute_name|
97
+ attribute(attribute_name)
98
+ end
99
+ end
48
100
 
49
- # Shorthand for defining multiple attributes
50
- #
51
- # @param attribute_names [Array<Symbol>] The names of the attributes to serialize
52
- #
53
- # @example
54
- # class UserSerializer < Transmutation::Serializer
55
- # attributes :first_name, :last_name
56
- # end
57
- def self.attributes(*attribute_names)
58
- attribute_names.each do |attr_name|
59
- attribute(attr_name)
101
+ # Shorthand for defining multiple associations
102
+ #
103
+ # @param association_names [Array<Symbol>] The names of the associations to serialize
104
+ #
105
+ # @example
106
+ # class UserSerializer < Transmutation::Serializer
107
+ # associations :posts, :comments
108
+ # end
109
+ def associations(*association_names)
110
+ association_names.each do |association_name|
111
+ association(association_name)
112
+ end
60
113
  end
61
114
  end
62
115
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Transmutation
4
- VERSION = "0.2.2"
4
+ VERSION = "0.3.0"
5
5
  end
@@ -16,6 +16,7 @@ Gem::Specification.new do |spec|
16
16
  spec.metadata["homepage_uri"] = spec.homepage
17
17
  spec.metadata["source_code_uri"] = "https://github.com/spellbook-technology/transmutation"
18
18
  spec.metadata["changelog_uri"] = "https://github.com/spellbook-technology/transmutation/CHANGELOG.md"
19
+ spec.metadata["documentation_uri"] = "https://rubydoc.info/gems/transmutation"
19
20
 
20
21
  # Specify which files should be added to the gem when it is released.
21
22
  # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: transmutation
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.2
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - nitemaeric
@@ -9,7 +9,7 @@ authors:
9
9
  autorequire:
10
10
  bindir: exe
11
11
  cert_chain: []
12
- date: 2024-06-05 00:00:00.000000000 Z
12
+ date: 2024-06-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: zeitwerk
@@ -46,7 +46,6 @@ files:
46
46
  - Rakefile
47
47
  - lib/transmutation.rb
48
48
  - lib/transmutation/class_attributes.rb
49
- - lib/transmutation/collection_serializer.rb
50
49
  - lib/transmutation/serialization.rb
51
50
  - lib/transmutation/serialization/lookup.rb
52
51
  - lib/transmutation/serialization/lookup/serializer_not_found.rb
@@ -62,6 +61,7 @@ metadata:
62
61
  homepage_uri: https://github.com/spellbook-technology/transmutation
63
62
  source_code_uri: https://github.com/spellbook-technology/transmutation
64
63
  changelog_uri: https://github.com/spellbook-technology/transmutation/CHANGELOG.md
64
+ documentation_uri: https://rubydoc.info/gems/transmutation
65
65
  post_install_message:
66
66
  rdoc_options: []
67
67
  require_paths:
@@ -1,31 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Transmutation
4
- # Out-of-the-box collection serializer.
5
- #
6
- # This serializer will be used to serialize all collections of objects.
7
- #
8
- # @example Basic usage
9
- # Transmutation::CollectionSerializer.new([object, object]).to_json
10
- class CollectionSerializer
11
- include Transmutation::Serialization
12
-
13
- def initialize(objects, namespace: nil, serializer: nil)
14
- @objects = objects
15
- @namespace = namespace
16
- @serializer = serializer
17
- end
18
-
19
- def as_json(options = {})
20
- objects.map { |item| serialize(item, namespace: namespace, serializer: serializer).as_json(options) }
21
- end
22
-
23
- def to_json(options = {})
24
- as_json(options).to_json
25
- end
26
-
27
- private
28
-
29
- attr_reader :objects, :namespace, :serializer
30
- end
31
- end