transmutation 0.1.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +2 -0
- data/.rubocop_todo.yml +16 -0
- data/Gemfile +2 -0
- data/Gemfile.lock +10 -2
- data/lib/transmutation/class_attributes.rb +41 -0
- data/lib/transmutation/collection_serializer.rb +12 -6
- data/lib/transmutation/serialization/lookup/serializer_not_found.rb +39 -0
- data/lib/transmutation/serialization/lookup.rb +89 -0
- data/lib/transmutation/serialization/rendering.rb +14 -0
- data/lib/transmutation/serialization.rb +30 -118
- data/lib/transmutation/serializer.rb +48 -15
- data/lib/transmutation/version.rb +1 -1
- data/lib/transmutation.rb +37 -0
- data/transmutation.gemspec +35 -0
- metadata +13 -8
- data/lib/transmutation/string_refinements.rb +0 -37
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f291b37c957ebb47d6d08fc4591d62b93f3cc03cd5bd6ca10dccaa2ad53e140e
|
4
|
+
data.tar.gz: a8df9a22118a10a20112555378756866bbe190ac6f643f7f24089f8327a3bfb2
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 50b1f81caaec7a83824e872d206843ebccd8e2bb143deedb64ffe7d512c2a0c8f8a22731be42f76ee5b10b4ceb249113005ca1246b90a75b0e136af24c2fc090
|
7
|
+
data.tar.gz: 5f66985fbddc2ac6251ee38595dc8c0345e1d8b11d9c36813dc93f86123b10529e045509dffd570bbc44eeb6903e5813f3f1e1f325a6581d61d04c8ed4291f3a
|
data/.rubocop.yml
CHANGED
data/.rubocop_todo.yml
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# This configuration was generated by
|
2
|
+
# `rubocop --auto-gen-config`
|
3
|
+
# on 2024-06-05 00:21:36 UTC using RuboCop version 1.63.4.
|
4
|
+
# The point is for the user to remove these configuration records
|
5
|
+
# one by one as the offenses are removed from the code base.
|
6
|
+
# Note that changes in the inspected code, or installation of new
|
7
|
+
# versions of RuboCop, may require this file to be generated again.
|
8
|
+
|
9
|
+
# Offense count: 2
|
10
|
+
# Configuration parameters: AllowedConstants.
|
11
|
+
Style/Documentation:
|
12
|
+
Exclude:
|
13
|
+
- 'spec/**/*'
|
14
|
+
- 'test/**/*'
|
15
|
+
- 'lib/transmutation/serialization.rb'
|
16
|
+
- 'lib/transmutation/serialization/rendering.rb'
|
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
transmutation (0.
|
4
|
+
transmutation (0.2.1)
|
5
5
|
zeitwerk (~> 2.6.15)
|
6
6
|
|
7
7
|
GEM
|
@@ -9,21 +9,27 @@ GEM
|
|
9
9
|
specs:
|
10
10
|
ast (2.4.2)
|
11
11
|
bigdecimal (3.1.7)
|
12
|
+
coderay (1.1.3)
|
12
13
|
diff-lcs (1.5.1)
|
13
14
|
docile (1.4.0)
|
14
15
|
imagen (0.1.8)
|
15
16
|
parser (>= 2.5, != 2.5.1.1)
|
16
17
|
json (2.7.2)
|
17
18
|
language_server-protocol (3.17.0.3)
|
19
|
+
method_source (1.1.0)
|
18
20
|
parallel (1.24.0)
|
19
21
|
parser (3.3.1.0)
|
20
22
|
ast (~> 2.4.1)
|
21
23
|
racc
|
24
|
+
pry (0.14.2)
|
25
|
+
coderay (~> 1.1)
|
26
|
+
method_source (~> 1.0)
|
22
27
|
racc (1.7.3)
|
23
28
|
rainbow (3.1.1)
|
24
29
|
rake (13.2.1)
|
25
30
|
regexp_parser (2.9.0)
|
26
|
-
rexml (3.2.
|
31
|
+
rexml (3.2.8)
|
32
|
+
strscan (>= 3.0.9)
|
27
33
|
rspec (3.13.0)
|
28
34
|
rspec-core (~> 3.13.0)
|
29
35
|
rspec-expectations (~> 3.13.0)
|
@@ -70,6 +76,7 @@ GEM
|
|
70
76
|
simplecov-html (0.12.3)
|
71
77
|
simplecov-lcov (0.8.0)
|
72
78
|
simplecov_json_formatter (0.1.4)
|
79
|
+
strscan (3.1.0)
|
73
80
|
undercover (0.5.0)
|
74
81
|
bigdecimal
|
75
82
|
imagen (>= 0.1.8)
|
@@ -83,6 +90,7 @@ PLATFORMS
|
|
83
90
|
ruby
|
84
91
|
|
85
92
|
DEPENDENCIES
|
93
|
+
pry
|
86
94
|
rake (~> 13.0)
|
87
95
|
rspec (~> 3.0)
|
88
96
|
rubocop (~> 1.21)
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transmutation
|
4
|
+
# @api private
|
5
|
+
module ClassAttributes
|
6
|
+
def class_attribute(
|
7
|
+
*names,
|
8
|
+
instance_accessor: true,
|
9
|
+
instance_reader: instance_accessor,
|
10
|
+
instance_writer: instance_accessor,
|
11
|
+
default: nil
|
12
|
+
)
|
13
|
+
class_attribute_reader(*names, instance_reader: instance_reader, default: default)
|
14
|
+
class_attribute_writer(*names, instance_writer: instance_writer, default: default)
|
15
|
+
end
|
16
|
+
|
17
|
+
def class_attribute_reader(*names, instance_reader: true, default: nil)
|
18
|
+
names.each do |name|
|
19
|
+
self.class.define_method(name) do
|
20
|
+
instance_variable_get("@#{name}")
|
21
|
+
end
|
22
|
+
|
23
|
+
define_method(name) { self.class.send(name) } if instance_reader
|
24
|
+
|
25
|
+
instance_variable_set("@#{name}", default)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def class_attribute_writer(*names, instance_writer: true, default: nil)
|
30
|
+
names.each do |name|
|
31
|
+
self.class.define_method("#{name}=") do |value|
|
32
|
+
instance_variable_set("@#{name}", value)
|
33
|
+
end
|
34
|
+
|
35
|
+
define_method("#{name}=") { |value| self.class.send("#{name}=", value) } if instance_writer
|
36
|
+
|
37
|
+
instance_variable_set("@#{name}", default)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -1,21 +1,27 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Transmutation
|
4
|
-
|
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
|
5
11
|
include Transmutation::Serialization
|
6
12
|
|
7
|
-
def initialize(objects, namespace:
|
13
|
+
def initialize(objects, namespace: nil, serializer: nil)
|
8
14
|
@objects = objects
|
9
15
|
@namespace = namespace
|
10
16
|
@serializer = serializer
|
11
17
|
end
|
12
18
|
|
13
19
|
def as_json(options = {})
|
14
|
-
|
20
|
+
objects.map { |item| serialize(item, namespace: namespace, serializer: serializer).as_json(options) }
|
21
|
+
end
|
15
22
|
|
16
|
-
|
17
|
-
|
18
|
-
end
|
23
|
+
def to_json(options = {})
|
24
|
+
as_json(options).to_json
|
19
25
|
end
|
20
26
|
|
21
27
|
private
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transmutation
|
4
|
+
module Serialization
|
5
|
+
class Lookup
|
6
|
+
# @api public
|
7
|
+
class SerializerNotFound < Transmutation::Error
|
8
|
+
attr_reader :object, :namespace, :name
|
9
|
+
|
10
|
+
def initialize(object, namespace: nil, name: nil)
|
11
|
+
@object = object
|
12
|
+
@namespace = namespace
|
13
|
+
@name = name
|
14
|
+
|
15
|
+
super [
|
16
|
+
"Couldn't find serializer for #{object.class.name}#{namespace.empty? ? "" : " in #{namespace}"}.",
|
17
|
+
"Tried looking for the following classes: #{attempted_lookups}."
|
18
|
+
].join(" ")
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def attempted_lookups
|
24
|
+
namespaces_chain.map { |namespace| [namespace, name].join("::") }.join(", ")
|
25
|
+
end
|
26
|
+
|
27
|
+
def namespaces_chain
|
28
|
+
@namespaces_chain ||= begin
|
29
|
+
namespace_parts = namespace.split("::")
|
30
|
+
|
31
|
+
namespace_parts.filter_map.with_index do |part, index|
|
32
|
+
[*namespace_parts[...index], part].join("::")
|
33
|
+
end.reverse
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,89 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transmutation
|
4
|
+
module Serialization
|
5
|
+
# @api private
|
6
|
+
class Lookup
|
7
|
+
def initialize(caller, namespace: nil)
|
8
|
+
@caller = caller
|
9
|
+
@namespace = namespace
|
10
|
+
end
|
11
|
+
|
12
|
+
# Bubbles up the namespace until we find a matching serializer.
|
13
|
+
#
|
14
|
+
# @see Transmutation::Serialization#lookup_serializer
|
15
|
+
#
|
16
|
+
# Example:
|
17
|
+
#
|
18
|
+
# namespace: Api::V1::Admin::Detailed
|
19
|
+
# serializer: Chat::User
|
20
|
+
#
|
21
|
+
# This method will attempt to find a serializer defined in the following order:
|
22
|
+
#
|
23
|
+
# - Api::V1::Admin::Detailed::Chat::UserSerializer
|
24
|
+
# - Api::V1::Admin::Chat::UserSerializer
|
25
|
+
# - Api::V1::Chat::UserSerializer
|
26
|
+
# - Api::Chat::UserSerializer
|
27
|
+
# - Chat::UserSerializer
|
28
|
+
#
|
29
|
+
# Note: This never bubbles up the object's namespace, only the caller's namespace.
|
30
|
+
def serializer_for(object, serializer: nil)
|
31
|
+
return Transmutation::CollectionSerializer if object.respond_to?(:map)
|
32
|
+
|
33
|
+
serializer_name = serializer_name_for(object, serializer: serializer)
|
34
|
+
|
35
|
+
return constantize_serializer!(Object, serializer_name, object: object) if serializer_name.start_with?("::")
|
36
|
+
|
37
|
+
potential_namespaces.each do |potential_namespace|
|
38
|
+
return potential_namespace.const_get(serializer_name) if potential_namespace.const_defined?(serializer_name)
|
39
|
+
end
|
40
|
+
|
41
|
+
raise SerializerNotFound.new(@object, namespace: serializer_namespace, name: serializer_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Returns the highest specificity serializer name for the given object.
|
45
|
+
#
|
46
|
+
# @param object [Object] The object to find the serializer name for.
|
47
|
+
#
|
48
|
+
# @return [String] The serializer name.
|
49
|
+
def serializer_name_for(object, serializer: nil)
|
50
|
+
return "::Transmutation::CollectionSerializer" if object.respond_to?(:map)
|
51
|
+
|
52
|
+
"#{serializer&.delete_suffix("Serializer") || object.class.name}Serializer"
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def potential_namespaces
|
58
|
+
@potential_namespaces ||= begin
|
59
|
+
namespace_parts = serializer_namespace.split("::")
|
60
|
+
|
61
|
+
namespace_parts.filter_map.with_index do |part, index|
|
62
|
+
namespace = [*namespace_parts[...index], part].join("::")
|
63
|
+
|
64
|
+
next if namespace.empty?
|
65
|
+
|
66
|
+
Object.const_get(namespace) if Object.const_defined?(namespace)
|
67
|
+
end.reverse
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
def serializer_namespace
|
72
|
+
return caller_namespace if @namespace.nil?
|
73
|
+
return @namespace if @namespace.start_with?("::")
|
74
|
+
|
75
|
+
"#{caller_namespace}::#{@namespace}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def caller_namespace
|
79
|
+
@caller_namespace ||= @caller.class.name.split("::")[...-1].join("::")
|
80
|
+
end
|
81
|
+
|
82
|
+
def constantize_serializer!(namespace, name, object:)
|
83
|
+
raise SerializerNotFound.new(object, namespace: namespace, name: name) unless namespace.const_defined?(name)
|
84
|
+
|
85
|
+
namespace.const_get(name)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Transmutation
|
4
|
+
module Serialization
|
5
|
+
module Rendering
|
6
|
+
def render(json: nil, serialize: true, **args)
|
7
|
+
return super(**args) unless json
|
8
|
+
return super(json: json, **args) unless serialize
|
9
|
+
|
10
|
+
super(**args, json: serialize(json))
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -1,123 +1,35 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
module Transmutation
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
"#{caller_namespace}::#{converted_namespace}::#{serializer_name}"
|
35
|
-
end
|
36
|
-
|
37
|
-
BY_CALLER_NAMESPACE_WITH_SERIALIZER = lambda do |caller, _resource_class, serializer:, **_|
|
38
|
-
caller_namespace = get_caller_namespace(caller.class)
|
39
|
-
|
40
|
-
converted_serializer = convert_serializer(serializer)
|
41
|
-
|
42
|
-
serializer_name = serializer_from_resource_name(converted_serializer)
|
43
|
-
|
44
|
-
"#{caller_namespace}::#{serializer_name}"
|
45
|
-
end
|
46
|
-
|
47
|
-
BY_RESOURCE_CALLER_NAMESPACE = lambda do |caller, resource_class, **_|
|
48
|
-
caller_namespace = get_caller_namespace(caller.class)
|
49
|
-
|
50
|
-
serializer_name = serializer_from_resource_name(resource_class)
|
51
|
-
|
52
|
-
"#{caller_namespace}::#{serializer_name}"
|
53
|
-
end
|
54
|
-
|
55
|
-
BY_RESOURCE = lambda do |_caller, resource_class, **_|
|
56
|
-
serializer_name = serializer_from_resource_name(resource_class)
|
57
|
-
|
58
|
-
"::#{serializer_name}"
|
59
|
-
end
|
60
|
-
|
61
|
-
LOOKUP_STRATEGIES = [
|
62
|
-
BY_CALLER_NAMESPACE_WITH_NAMESPACE_AND_SERIALIZER,
|
63
|
-
BY_RESOURCE_CALLER_NAMESPACE_WITH_NAMESPACE,
|
64
|
-
BY_CALLER_NAMESPACE_WITH_SERIALIZER,
|
65
|
-
BY_RESOURCE_CALLER_NAMESPACE,
|
66
|
-
BY_RESOURCE
|
67
|
-
].freeze
|
68
|
-
|
69
|
-
def lookup_serializer(object, namespace: "", serializer: nil)
|
70
|
-
lookup_serializer!(object, namespace: namespace, serializer: serializer)
|
71
|
-
rescue NameError
|
72
|
-
nil
|
73
|
-
end
|
74
|
-
|
75
|
-
def render(**args)
|
76
|
-
return super(**args) unless args[:json]
|
77
|
-
return super(**args) if args[:serialize] == false
|
78
|
-
|
79
|
-
super(**args, json: serialize(args[:json]))
|
80
|
-
end
|
81
|
-
|
82
|
-
def lookup_serializer!(object, namespace: "", serializer: nil)
|
83
|
-
LOOKUP_STRATEGIES.each do |strategy|
|
84
|
-
return Object.const_get(strategy.call(self, object.class, namespace: namespace, serializer: serializer))
|
85
|
-
rescue NameError
|
86
|
-
next
|
87
|
-
end
|
88
|
-
|
89
|
-
raise NameError, "Serializer not found for #{object.class}"
|
90
|
-
end
|
91
|
-
|
92
|
-
def serialize(object, namespace: "", serializer: nil)
|
93
|
-
unless object.respond_to?(:map)
|
94
|
-
return lookup_serializer!(object, namespace: namespace,
|
95
|
-
serializer: serializer).new(object)
|
96
|
-
end
|
97
|
-
|
98
|
-
object.map do |entry_object|
|
99
|
-
lookup_serializer!(entry_object, namespace: namespace, serializer: serializer).new(entry_object)
|
100
|
-
end
|
101
|
-
end
|
102
|
-
|
103
|
-
module_function
|
104
|
-
|
105
|
-
def get_caller_namespace(base)
|
106
|
-
base.class_eval do
|
107
|
-
@caller_namespace
|
108
|
-
end
|
109
|
-
end
|
110
|
-
|
111
|
-
def convert_namespace(namespace)
|
112
|
-
namespace.to_s.camelcase
|
113
|
-
end
|
114
|
-
|
115
|
-
def convert_serializer(serializer)
|
116
|
-
serializer.to_s.camelcase.gsub("Serializer", "")
|
117
|
-
end
|
118
|
-
|
119
|
-
def serializer_from_resource_name(name)
|
120
|
-
"#{name}Serializer"
|
3
|
+
module Transmutation
|
4
|
+
module Serialization
|
5
|
+
# Serialize a given object with the looked up serializer.
|
6
|
+
#
|
7
|
+
#
|
8
|
+
# @param object [Object] The object to serialize.
|
9
|
+
# @param namespace [String, Symbol, Module] The namespace to lookup the serializer in.
|
10
|
+
# @param serializer [String, Symbol, Class] The serializer to use.
|
11
|
+
#
|
12
|
+
# @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)
|
15
|
+
end
|
16
|
+
|
17
|
+
# Lookup the serializer for the given object.
|
18
|
+
#
|
19
|
+
# This calls {Transmutation::Serialization::Lookup#serializer_for} to find the serializer for the given object.
|
20
|
+
#
|
21
|
+
# @param object [Object] The object to lookup the serializer for.
|
22
|
+
# @param namespace [String, Symbol, Module] The namespace to lookup the serializer in.
|
23
|
+
# @param serializer [String, Symbol, Class] The serializer to use.
|
24
|
+
#
|
25
|
+
# @return [Class<Transmutation::Serializer>] The serializer for the given object.
|
26
|
+
#
|
27
|
+
def lookup_serializer(object, namespace: nil, serializer: nil)
|
28
|
+
Lookup.new(self, namespace: namespace).serializer_for(object, serializer: serializer)
|
29
|
+
end
|
30
|
+
|
31
|
+
private_class_method def self.included(base)
|
32
|
+
base.include(Rendering) if base.respond_to?(:render)
|
121
33
|
end
|
122
34
|
end
|
123
35
|
end
|
@@ -1,7 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module Transmutation
|
4
|
-
class
|
4
|
+
# Base class for your serializers.
|
5
|
+
#
|
6
|
+
# @example Basic usage
|
7
|
+
# class UserSerializer < Transmutation::Serializer
|
8
|
+
# attributes :first_name, :last_name
|
9
|
+
#
|
10
|
+
# attribute :full_name do
|
11
|
+
# "#{object.first_name} #{object.last_name}".strip
|
12
|
+
# end
|
13
|
+
# end
|
14
|
+
class Serializer
|
15
|
+
extend ClassAttributes
|
16
|
+
|
5
17
|
def initialize(object)
|
6
18
|
@object = object
|
7
19
|
end
|
@@ -11,31 +23,52 @@ module Transmutation
|
|
11
23
|
end
|
12
24
|
|
13
25
|
def as_json(_options = {})
|
14
|
-
|
26
|
+
attributes_config.each_with_object({}) do |(attr_name, attr_options), hash|
|
15
27
|
hash[attr_name.to_s] = attr_options[:block] ? instance_exec(&attr_options[:block]) : object.send(attr_name)
|
16
28
|
end
|
17
29
|
end
|
18
30
|
|
19
|
-
|
20
|
-
|
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 }
|
21
47
|
end
|
22
48
|
|
23
|
-
|
24
|
-
|
25
|
-
|
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)
|
26
60
|
end
|
27
61
|
end
|
28
62
|
|
29
|
-
def self._attributes
|
30
|
-
@@attributes ||= {} # rubocop:disable Style/ClassVars
|
31
|
-
end
|
32
|
-
|
33
|
-
def _attributes
|
34
|
-
self.class._attributes
|
35
|
-
end
|
36
|
-
|
37
63
|
private
|
38
64
|
|
65
|
+
class_attribute :attributes_config, instance_writer: false, default: {}
|
66
|
+
|
39
67
|
attr_reader :object
|
68
|
+
|
69
|
+
private_class_method def self.inherited(subclass)
|
70
|
+
super
|
71
|
+
subclass.attributes_config = attributes_config.dup
|
72
|
+
end
|
40
73
|
end
|
41
74
|
end
|
data/lib/transmutation.rb
CHANGED
@@ -4,6 +4,43 @@ require "zeitwerk"
|
|
4
4
|
loader = Zeitwerk::Loader.for_gem
|
5
5
|
loader.setup
|
6
6
|
|
7
|
+
# A performant and expressive solution for serializing Ruby objects into JSON, with a touch of opinionated "magic" ✨.
|
8
|
+
#
|
9
|
+
# @example Basic usage
|
10
|
+
# # Define a data class.
|
11
|
+
# class User
|
12
|
+
# attr_reader :name, :email
|
13
|
+
#
|
14
|
+
# def initialize(name:, email:)
|
15
|
+
# @name = name
|
16
|
+
# @email = email
|
17
|
+
# end
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# # Define a serializer.
|
21
|
+
# class UserSerializer < Transmutation::Serializer
|
22
|
+
# attribute :name
|
23
|
+
# end
|
24
|
+
#
|
25
|
+
# # Create an instance of the data class.
|
26
|
+
# user = User.new(name: "John", email: "john@example.com")
|
27
|
+
#
|
28
|
+
# # Serialize the data class instance.
|
29
|
+
# UserSerializer.new(user).to_json # => "{\"name\":\"John\"}"
|
30
|
+
#
|
31
|
+
# @example Within a Rails controller
|
32
|
+
# class UsersController < ApplicationController
|
33
|
+
# include Transmutation::Serialization
|
34
|
+
#
|
35
|
+
# def show
|
36
|
+
# user = User.find(params[:id])
|
37
|
+
#
|
38
|
+
# # Automatically lookup the UserSerializer
|
39
|
+
# # Serialize the data class instance using the UserSerializer
|
40
|
+
# # Render the result as JSON to the client
|
41
|
+
# render json: user # => "{\"name\":\"John\"}"
|
42
|
+
# end
|
43
|
+
# end
|
7
44
|
module Transmutation
|
8
45
|
class Error < StandardError; end
|
9
46
|
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "lib/transmutation/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |spec|
|
6
|
+
spec.name = "transmutation"
|
7
|
+
spec.version = Transmutation::VERSION
|
8
|
+
spec.authors = %w[nitemaeric borrabeer]
|
9
|
+
spec.email = %w[daniel@spellbook.tech worapath.pakkavesa@spellbook.tech]
|
10
|
+
|
11
|
+
spec.summary = "Ruby JSON serialization library"
|
12
|
+
spec.homepage = "https://github.com/spellbook-technology/transmutation"
|
13
|
+
spec.license = "MIT"
|
14
|
+
spec.required_ruby_version = ">= 3.0"
|
15
|
+
|
16
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
17
|
+
spec.metadata["source_code_uri"] = "https://github.com/spellbook-technology/transmutation"
|
18
|
+
spec.metadata["changelog_uri"] = "https://github.com/spellbook-technology/transmutation/CHANGELOG.md"
|
19
|
+
|
20
|
+
# Specify which files should be added to the gem when it is released.
|
21
|
+
# The `git ls-files -z` loads the files in the RubyGem that have been added into git.
|
22
|
+
spec.files = Dir.chdir(__dir__) do
|
23
|
+
`git ls-files -z`.split("\x0").reject do |f|
|
24
|
+
(f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
|
25
|
+
end
|
26
|
+
end
|
27
|
+
spec.bindir = "exe"
|
28
|
+
spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
|
29
|
+
spec.require_paths = ["lib"]
|
30
|
+
|
31
|
+
spec.add_dependency "zeitwerk", "~> 2.6.15"
|
32
|
+
|
33
|
+
# For more information and examples about making a new gem, check out our
|
34
|
+
# guide at: https://bundler.io/guides/creating_gem.html
|
35
|
+
end
|
metadata
CHANGED
@@ -1,15 +1,15 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: transmutation
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- nitemaeric
|
8
8
|
- borrabeer
|
9
|
-
autorequire:
|
9
|
+
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2024-05
|
12
|
+
date: 2024-06-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: zeitwerk
|
@@ -25,7 +25,7 @@ dependencies:
|
|
25
25
|
- - "~>"
|
26
26
|
- !ruby/object:Gem::Version
|
27
27
|
version: 2.6.15
|
28
|
-
description:
|
28
|
+
description:
|
29
29
|
email:
|
30
30
|
- daniel@spellbook.tech
|
31
31
|
- worapath.pakkavesa@spellbook.tech
|
@@ -35,6 +35,7 @@ extra_rdoc_files: []
|
|
35
35
|
files:
|
36
36
|
- ".rspec"
|
37
37
|
- ".rubocop.yml"
|
38
|
+
- ".rubocop_todo.yml"
|
38
39
|
- ".ruby-version"
|
39
40
|
- CHANGELOG.md
|
40
41
|
- CODE_OF_CONDUCT.md
|
@@ -44,12 +45,16 @@ files:
|
|
44
45
|
- README.md
|
45
46
|
- Rakefile
|
46
47
|
- lib/transmutation.rb
|
48
|
+
- lib/transmutation/class_attributes.rb
|
47
49
|
- lib/transmutation/collection_serializer.rb
|
48
50
|
- lib/transmutation/serialization.rb
|
51
|
+
- lib/transmutation/serialization/lookup.rb
|
52
|
+
- lib/transmutation/serialization/lookup/serializer_not_found.rb
|
53
|
+
- lib/transmutation/serialization/rendering.rb
|
49
54
|
- lib/transmutation/serializer.rb
|
50
|
-
- lib/transmutation/string_refinements.rb
|
51
55
|
- lib/transmutation/version.rb
|
52
56
|
- sig/transmutation.rbs
|
57
|
+
- transmutation.gemspec
|
53
58
|
homepage: https://github.com/spellbook-technology/transmutation
|
54
59
|
licenses:
|
55
60
|
- MIT
|
@@ -57,7 +62,7 @@ metadata:
|
|
57
62
|
homepage_uri: https://github.com/spellbook-technology/transmutation
|
58
63
|
source_code_uri: https://github.com/spellbook-technology/transmutation
|
59
64
|
changelog_uri: https://github.com/spellbook-technology/transmutation/CHANGELOG.md
|
60
|
-
post_install_message:
|
65
|
+
post_install_message:
|
61
66
|
rdoc_options: []
|
62
67
|
require_paths:
|
63
68
|
- lib
|
@@ -72,8 +77,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
72
77
|
- !ruby/object:Gem::Version
|
73
78
|
version: '0'
|
74
79
|
requirements: []
|
75
|
-
rubygems_version: 3.
|
76
|
-
signing_key:
|
80
|
+
rubygems_version: 3.5.9
|
81
|
+
signing_key:
|
77
82
|
specification_version: 4
|
78
83
|
summary: Ruby JSON serialization library
|
79
84
|
test_files: []
|
@@ -1,37 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Transmutation
|
4
|
-
module StringRefinements # rubocop:disable Style/Documentation
|
5
|
-
DELIMITERS = %r{[a-z][A-Z]|\s*-\s*|\s*/\s*|\s*:+\s*|\s*_\s*|\s+}
|
6
|
-
|
7
|
-
refine String do
|
8
|
-
def camelcase
|
9
|
-
return capitalize unless match? DELIMITERS
|
10
|
-
|
11
|
-
split(%r{\s*-\s*|\s*/\s*|\s*:+\s*}).then { |parts| combine parts, :capitalize, "::" }
|
12
|
-
.then { |text| text.split(/\s*_\s*|\s+/) }
|
13
|
-
.then { |parts| combine parts, :capitalize }
|
14
|
-
end
|
15
|
-
|
16
|
-
def first(maximum = 0)
|
17
|
-
return self if empty?
|
18
|
-
return self[0] if maximum.zero?
|
19
|
-
return "" if maximum.negative?
|
20
|
-
|
21
|
-
self[..(maximum - 1)]
|
22
|
-
end
|
23
|
-
|
24
|
-
def capitalize = empty? ? self : first.upcase + self[1, size]
|
25
|
-
|
26
|
-
private
|
27
|
-
|
28
|
-
def combine(parts, method, delimiter = "")
|
29
|
-
parts.reduce "" do |result, part|
|
30
|
-
next part.public_send method if result.empty?
|
31
|
-
|
32
|
-
"#{result}#{delimiter}#{part.__send__ method}"
|
33
|
-
end
|
34
|
-
end
|
35
|
-
end
|
36
|
-
end
|
37
|
-
end
|