transmutation 0.1.1 → 0.2.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/.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
- metadata +7 -3
- 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: 0dd356719d0c48951dd08d327c72b1b94b7b7ef4437c105fcbc7a22558bf4c14
|
4
|
+
data.tar.gz: efc9997ee8be40ce11894d7a64e570e7d53f617a3251bebb48c4f940050065be
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 93784f5a82ac13bcb4dc2560a12fccdee59135e5576dad82a556f4cb5f709811b7909aaa024273e1a38c1900d0534f47abc0f3ff3410c0b3dbce215365f31b84
|
7
|
+
data.tar.gz: 29daa074911fb3d8f984a5088963686155939093126a467940e54bd9d836b00705bfd6c13e54f1d2ed5199c4bdff18fc4d513ec9e692102f6065726d3a9fd994
|
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.0)
|
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
|
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.2.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-05
|
12
|
+
date: 2024-06-05 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: zeitwerk
|
@@ -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,10 +45,13 @@ 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
|
53
57
|
homepage: https://github.com/spellbook-technology/transmutation
|
@@ -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
|