transmutation 0.6.1 → 0.7.0
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 +4 -4
- data/lib/transmutation/association.rb +33 -0
- data/lib/transmutation/attribute.rb +13 -0
- data/lib/transmutation/field.rb +36 -0
- data/lib/transmutation/serialization/rendering.rb +2 -1
- data/lib/transmutation/serialization.rb +31 -5
- data/lib/transmutation/serializer.rb +27 -20
- data/lib/transmutation/version.rb +1 -1
- metadata +5 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 55d12ac9787b8517a2e9cc9a5b94a21877e040d74871aa419f7cb916a56b8466
|
|
4
|
+
data.tar.gz: 5e9ba27e9d68248e735db47f1bcfdf57224a19276ad9e6d4ad3261c5721b4919
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 11f50b0905cb83e1abdbe37e7e7f8a5dc9ae021277fe931497255e169f6d23e74032befa82c5014402b30daf38bd536b4f015f40dcdcee7028b6dfc18ccef645
|
|
7
|
+
data.tar.gz: 19b616c7aa56b1fce83d1089df02d5ddeeb170d0a5c7b1259fd7264b8100dc57c20f0b2497592b880dbbaa306769e4abd271aae0b14474d88d0d44c3527a1780
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Transmutation
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# An association: resolves the associated object (via a block or a method on the object) and
|
|
7
|
+
# serializes it with the looked up serializer, one level deeper. Only rendered within `max_depth`.
|
|
8
|
+
class Association < Field
|
|
9
|
+
def initialize(name, namespace: nil, serializer: nil, **options, &block)
|
|
10
|
+
super(name, **options, &block)
|
|
11
|
+
|
|
12
|
+
@namespace = namespace
|
|
13
|
+
@serializer = serializer
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def render?(serializer)
|
|
17
|
+
super && serializer.depth < serializer.max_depth
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def value(serializer, options)
|
|
21
|
+
target = block ? serializer.instance_exec(&block) : serializer.object.send(name)
|
|
22
|
+
|
|
23
|
+
serializer.serialize(
|
|
24
|
+
target,
|
|
25
|
+
namespace: @namespace,
|
|
26
|
+
serializer: @serializer,
|
|
27
|
+
depth: serializer.depth + 1,
|
|
28
|
+
max_depth: serializer.max_depth,
|
|
29
|
+
context: serializer.context
|
|
30
|
+
).as_json(options)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Transmutation
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# A plain attribute: either a block evaluated in the serializer's context, or a method sent to the
|
|
7
|
+
# serialized object.
|
|
8
|
+
class Attribute < Field
|
|
9
|
+
def value(serializer, _options)
|
|
10
|
+
block ? serializer.instance_exec(&block) : serializer.object.send(name)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Transmutation
|
|
4
|
+
# @api private
|
|
5
|
+
#
|
|
6
|
+
# Base class for a serializable field. Encapsulates the name, the precomputed JSON key, and the
|
|
7
|
+
# optional `if:`/`unless:` rendering conditions shared by attributes and associations.
|
|
8
|
+
class Field
|
|
9
|
+
attr_reader :name, :key
|
|
10
|
+
|
|
11
|
+
def initialize(name, **options, &block)
|
|
12
|
+
@name = name
|
|
13
|
+
@key = name.to_s.freeze
|
|
14
|
+
@block = block
|
|
15
|
+
@if = options[:if]
|
|
16
|
+
@unless = options[:unless]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Whether this field should be rendered for the given serializer instance.
|
|
20
|
+
def render?(serializer)
|
|
21
|
+
return false if @if && !evaluate(@if, serializer)
|
|
22
|
+
return false if @unless && evaluate(@unless, serializer)
|
|
23
|
+
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
attr_reader :block
|
|
30
|
+
|
|
31
|
+
# A Symbol condition is sent to the serializer; anything else (e.g. a Proc) runs in its context.
|
|
32
|
+
def evaluate(condition, serializer)
|
|
33
|
+
condition.is_a?(Symbol) ? serializer.send(condition) : serializer.instance_exec(&condition)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -19,11 +19,12 @@ module Transmutation
|
|
|
19
19
|
namespace = kwargs.delete(:namespace)
|
|
20
20
|
serializer = kwargs.delete(:serializer)
|
|
21
21
|
max_depth = kwargs.delete(:max_depth) || Transmutation.max_depth
|
|
22
|
+
context = kwargs.delete(:context) { self }
|
|
22
23
|
|
|
23
24
|
return super(**kwargs) unless json
|
|
24
25
|
return super(**kwargs, json:) unless should_serialize
|
|
25
26
|
|
|
26
|
-
super(**kwargs, json: serialize(json, namespace:, serializer:, max_depth:))
|
|
27
|
+
super(**kwargs, json: serialize(json, namespace:, serializer:, max_depth:, context:))
|
|
27
28
|
end
|
|
28
29
|
end
|
|
29
30
|
end
|
|
@@ -8,14 +8,14 @@ module Transmutation
|
|
|
8
8
|
# @param namespace [String, Symbol, Module] The namespace to lookup the serializer in.
|
|
9
9
|
# @param serializer [String, Symbol, Class] The serializer to use.
|
|
10
10
|
# @param max_depth [Integer] The maximum depth of nested associations to serialize.
|
|
11
|
+
# @param context [Object] Arbitrary context made available to the serializer as `#context`.
|
|
12
|
+
# Defaults to the caller (e.g. the controller), so serializers can reach `context.current_user`.
|
|
11
13
|
#
|
|
12
14
|
# @return [Transmutation::Serializer] The serialized object. This will respond to `#as_json` and `#to_json`.
|
|
13
|
-
def serialize(object, namespace: nil, serializer: nil, depth: 0, max_depth: Transmutation.max_depth)
|
|
14
|
-
|
|
15
|
-
return object.map { |item| serialize(item, namespace:, serializer:, depth:, max_depth:) }
|
|
16
|
-
end
|
|
15
|
+
def serialize(object, namespace: nil, serializer: nil, depth: 0, max_depth: Transmutation.max_depth, context: self)
|
|
16
|
+
return serialize_collection(object, namespace:, serializer:, depth:, max_depth:, context:) if collection?(object)
|
|
17
17
|
|
|
18
|
-
lookup_serializer(object, namespace:, serializer:).new(object, depth:, max_depth:)
|
|
18
|
+
lookup_serializer(object, namespace:, serializer:).new(object, depth:, max_depth:, context:)
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
# Lookup the serializer for the given object.
|
|
@@ -50,6 +50,32 @@ module Transmutation
|
|
|
50
50
|
@namespace ||= self.class.name.to_s[0, self.class.name.rindex("::") || 0]
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def collection?(object)
|
|
56
|
+
object.respond_to?(:map) && !object.respond_to?(:to_hash)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Serialize each item in a collection.
|
|
60
|
+
#
|
|
61
|
+
# Items of the same class resolve to the same serializer, so the lookup is memoised on the item's
|
|
62
|
+
# class and reused across siblings rather than rebuilding the cache key (and hashing it) per item.
|
|
63
|
+
def serialize_collection(collection, namespace:, serializer:, depth:, max_depth:, context:)
|
|
64
|
+
serializer_class = nil
|
|
65
|
+
serialized_class = nil
|
|
66
|
+
|
|
67
|
+
collection.map do |item|
|
|
68
|
+
next serialize(item, namespace:, serializer:, depth:, max_depth:, context:) if collection?(item)
|
|
69
|
+
|
|
70
|
+
if item.class != serialized_class
|
|
71
|
+
serializer_class = lookup_serializer(item, namespace:, serializer:)
|
|
72
|
+
serialized_class = item.class
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
serializer_class.new(item, depth:, max_depth:, context:)
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
53
79
|
private_class_method def self.included(base)
|
|
54
80
|
base.include(Rendering) if base.method_defined?(:render)
|
|
55
81
|
end
|
|
@@ -20,10 +20,11 @@ module Transmutation
|
|
|
20
20
|
|
|
21
21
|
include Transmutation::Serialization
|
|
22
22
|
|
|
23
|
-
def initialize(object, depth: 0, max_depth: 1)
|
|
23
|
+
def initialize(object, depth: 0, max_depth: 1, context: nil)
|
|
24
24
|
@object = object
|
|
25
25
|
@depth = depth
|
|
26
26
|
@max_depth = max_depth
|
|
27
|
+
@context = context
|
|
27
28
|
end
|
|
28
29
|
|
|
29
30
|
def to_json(options = {})
|
|
@@ -31,12 +32,8 @@ module Transmutation
|
|
|
31
32
|
end
|
|
32
33
|
|
|
33
34
|
def as_json(options = {})
|
|
34
|
-
|
|
35
|
-
if
|
|
36
|
-
hash[attr_name.to_s] = instance_exec(&attr_options[:block]).as_json(options) 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
|
|
35
|
+
fields.each_with_object({}) do |field, hash|
|
|
36
|
+
hash[field.key] = field.value(self, options) if field.render?(self)
|
|
40
37
|
end
|
|
41
38
|
end
|
|
42
39
|
|
|
@@ -44,6 +41,9 @@ module Transmutation
|
|
|
44
41
|
# Define an attribute to be serialized
|
|
45
42
|
#
|
|
46
43
|
# @param attribute_name [Symbol] The name of the attribute to serialize
|
|
44
|
+
# @param if [Symbol, Proc] Only include the attribute when the condition evaluates truthy
|
|
45
|
+
# @param unless [Symbol, Proc] Exclude the attribute when the condition evaluates truthy
|
|
46
|
+
# - A Symbol is sent to the serializer instance, a Proc is evaluated in its context
|
|
47
47
|
# @yield [object] The block to call to get the value of the attribute
|
|
48
48
|
# - The block is called in the context of the serializer instance
|
|
49
49
|
#
|
|
@@ -54,9 +54,11 @@ module Transmutation
|
|
|
54
54
|
# attribute :full_name do
|
|
55
55
|
# "#{object.first_name} #{object.last_name}".strip
|
|
56
56
|
# end
|
|
57
|
+
#
|
|
58
|
+
# attribute :email, if: :admin?
|
|
57
59
|
# end
|
|
58
|
-
def attribute(attribute_name, &block)
|
|
59
|
-
|
|
60
|
+
def attribute(attribute_name, **options, &block)
|
|
61
|
+
fields[attribute_name] = Attribute.new(attribute_name, **options, &block)
|
|
60
62
|
end
|
|
61
63
|
|
|
62
64
|
# Define an association to be serialized
|
|
@@ -66,6 +68,8 @@ module Transmutation
|
|
|
66
68
|
# @param association_name [Symbol] The name of the association to serialize
|
|
67
69
|
# @param namespace [String, Symbol, Module] The namespace to lookup the association's serializer in
|
|
68
70
|
# @param serializer [String, Symbol, Class] The serializer to use for the association's serialization
|
|
71
|
+
# @param if [Symbol, Proc] Only include the association when the condition evaluates truthy
|
|
72
|
+
# @param unless [Symbol, Proc] Exclude the association when the condition evaluates truthy
|
|
69
73
|
# @yield [object] The block to call to get the value of the association
|
|
70
74
|
# - The block is called in the context of the serializer instance
|
|
71
75
|
# - The return value from the block is automatically serialized
|
|
@@ -78,14 +82,8 @@ module Transmutation
|
|
|
78
82
|
# object.posts.archived
|
|
79
83
|
# end
|
|
80
84
|
# end
|
|
81
|
-
def association(association_name, namespace: nil, serializer: nil, &
|
|
82
|
-
|
|
83
|
-
association_instance = custom_block ? instance_exec(&custom_block) : object.send(association_name)
|
|
84
|
-
|
|
85
|
-
serialize(association_instance, namespace:, serializer:, depth: @depth + 1, max_depth: @max_depth)
|
|
86
|
-
end
|
|
87
|
-
|
|
88
|
-
attributes_config[association_name] = { block:, association: true }
|
|
85
|
+
def association(association_name, namespace: nil, serializer: nil, **options, &block)
|
|
86
|
+
fields[association_name] = Association.new(association_name, namespace:, serializer:, **options, &block)
|
|
89
87
|
end
|
|
90
88
|
|
|
91
89
|
# Shorthand for defining multiple attributes
|
|
@@ -121,15 +119,24 @@ module Transmutation
|
|
|
121
119
|
alias has_many associations
|
|
122
120
|
end
|
|
123
121
|
|
|
122
|
+
# @return [Object] the object being serialized
|
|
123
|
+
# @return [Integer] depth the current nesting depth
|
|
124
|
+
# @return [Integer] max_depth the maximum nesting depth to serialize
|
|
125
|
+
# @return [Object, nil] context the caller-supplied context (defaults to the caller of `serialize`)
|
|
126
|
+
attr_reader :object, :depth, :max_depth, :context
|
|
127
|
+
|
|
124
128
|
private
|
|
125
129
|
|
|
126
|
-
class_attribute :
|
|
130
|
+
class_attribute :fields, instance_accessor: false, default: {}
|
|
127
131
|
|
|
128
|
-
|
|
132
|
+
# The fields declared on this serializer, in declaration order.
|
|
133
|
+
def fields
|
|
134
|
+
self.class.fields.values
|
|
135
|
+
end
|
|
129
136
|
|
|
130
137
|
private_class_method def self.inherited(subclass)
|
|
131
138
|
super
|
|
132
|
-
subclass.
|
|
139
|
+
subclass.fields = fields.dup
|
|
133
140
|
end
|
|
134
141
|
end
|
|
135
142
|
end
|
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.
|
|
4
|
+
version: 0.7.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: 2026-
|
|
12
|
+
date: 2026-07-09 00:00:00.000000000 Z
|
|
13
13
|
dependencies:
|
|
14
14
|
- !ruby/object:Gem::Dependency
|
|
15
15
|
name: activesupport
|
|
@@ -48,7 +48,10 @@ extensions: []
|
|
|
48
48
|
extra_rdoc_files: []
|
|
49
49
|
files:
|
|
50
50
|
- lib/transmutation.rb
|
|
51
|
+
- lib/transmutation/association.rb
|
|
52
|
+
- lib/transmutation/attribute.rb
|
|
51
53
|
- lib/transmutation/class_attributes.rb
|
|
54
|
+
- lib/transmutation/field.rb
|
|
52
55
|
- lib/transmutation/object_serializer.rb
|
|
53
56
|
- lib/transmutation/serialization.rb
|
|
54
57
|
- lib/transmutation/serialization/lookup.rb
|