porridge 0.1.0 → 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/.gitignore +3 -0
- data/.idea/fileTemplates/includes/Ruby File Header.rb +2 -0
- data/.idea/fileTemplates/internal/RSpec.rb +5 -0
- data/.idea/porridge.iml +6 -0
- data/.yardopts +1 -0
- data/CHANGELOG.md +4 -0
- data/Gemfile.lock +14 -1
- data/README.md +2 -1
- data/lib/porridge/array_serializer.rb +55 -0
- data/lib/porridge/chain_serializer.rb +41 -0
- data/lib/porridge/error.rb +8 -0
- data/lib/porridge/extractor.rb +33 -0
- data/lib/porridge/factory.rb +80 -0
- data/lib/porridge/field_policy.rb +38 -0
- data/lib/porridge/field_serializer.rb +88 -0
- data/lib/porridge/invalid_extractor_error.rb +6 -0
- data/lib/porridge/invalid_field_policy_error.rb +6 -0
- data/lib/porridge/invalid_serializer_error.rb +6 -0
- data/lib/porridge/key_normalizing_serializer.rb +51 -0
- data/lib/porridge/send_extractor.rb +27 -0
- data/lib/porridge/serializer.rb +43 -0
- data/lib/porridge/serializer_definer.rb +73 -0
- data/lib/porridge/serializer_definition.rb +30 -0
- data/lib/porridge/serializer_for_extracted.rb +43 -0
- data/lib/porridge/serializer_with_root.rb +86 -0
- data/lib/porridge/serializing_extractor.rb +42 -0
- data/lib/porridge/version.rb +1 -1
- data/lib/porridge/whitelist_field_policy.rb +70 -0
- data/lib/porridge.rb +21 -4
- data/porridge.gemspec +2 -0
- metadata +38 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1c767e7225acd4284055aa8379a67e9f8f8527d8946ff4df782e3c377d67bec8
|
|
4
|
+
data.tar.gz: b2bad4d91dc0feb01d990aaa1226345e49ce9baf0900578de6f720d47f86d5c3
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 835d2f5b110f0de84a242416791b49b28847de02cab5324f87ea6ee6e6209236f9fbc9e288e3caa158056f8e060e9ff254bf48a73c3b925366ea4772fe11be78
|
|
7
|
+
data.tar.gz: bee59698bf34b8f763c36f39955ffa15ae60976cd151675bbfe2e049f6eb8ba4205d99db74d069b181c64636584d3dbc5816097ceafb9d9f081e7a6ac084d98a
|
data/.gitignore
CHANGED
data/.idea/porridge.iml
CHANGED
|
@@ -11,12 +11,16 @@
|
|
|
11
11
|
</content>
|
|
12
12
|
<orderEntry type="inheritedJdk" />
|
|
13
13
|
<orderEntry type="sourceFolder" forTests="false" />
|
|
14
|
+
<orderEntry type="library" scope="PROVIDED" name="activesupport (v5.2.5, rbenv: 3.0.0) [gem]" level="application" />
|
|
14
15
|
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, rbenv: 3.0.0) [gem]" level="application" />
|
|
15
16
|
<orderEntry type="library" scope="PROVIDED" name="bundler (v2.2.25, rbenv: 3.0.0) [gem]" level="application" />
|
|
16
17
|
<orderEntry type="library" scope="PROVIDED" name="byebug (v11.1.3, rbenv: 3.0.0) [gem]" level="application" />
|
|
17
18
|
<orderEntry type="library" scope="PROVIDED" name="codecov (v0.6.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
19
|
+
<orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.1.9, rbenv: 3.0.0) [gem]" level="application" />
|
|
18
20
|
<orderEntry type="library" scope="PROVIDED" name="diff-lcs (v1.5.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
19
21
|
<orderEntry type="library" scope="PROVIDED" name="docile (v1.4.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
22
|
+
<orderEntry type="library" scope="PROVIDED" name="i18n (v1.8.11, rbenv: 3.0.0) [gem]" level="application" />
|
|
23
|
+
<orderEntry type="library" scope="PROVIDED" name="minitest (v5.15.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
20
24
|
<orderEntry type="library" scope="PROVIDED" name="parallel (v1.21.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
21
25
|
<orderEntry type="library" scope="PROVIDED" name="parser (v3.1.0.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
22
26
|
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.0.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
@@ -36,6 +40,8 @@
|
|
|
36
40
|
<orderEntry type="library" scope="PROVIDED" name="simplecov (v0.21.2, rbenv: 3.0.0) [gem]" level="application" />
|
|
37
41
|
<orderEntry type="library" scope="PROVIDED" name="simplecov-html (v0.12.3, rbenv: 3.0.0) [gem]" level="application" />
|
|
38
42
|
<orderEntry type="library" scope="PROVIDED" name="simplecov_json_formatter (v0.1.3, rbenv: 3.0.0) [gem]" level="application" />
|
|
43
|
+
<orderEntry type="library" scope="PROVIDED" name="thread_safe (v0.3.6, rbenv: 3.0.0) [gem]" level="application" />
|
|
44
|
+
<orderEntry type="library" scope="PROVIDED" name="tzinfo (v1.2.9, rbenv: 3.0.0) [gem]" level="application" />
|
|
39
45
|
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v2.1.0, rbenv: 3.0.0) [gem]" level="application" />
|
|
40
46
|
</component>
|
|
41
47
|
<component name="RakeTasksCache">
|
data/.yardopts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
--private --protected lib/**/*.rb - README.md
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.2.0] - 2022-01-19
|
|
4
|
+
|
|
5
|
+
This is the initial functional release of the gem. Extractors, serializers, fields, field policies, and an elegant DSL over top were implemented added.
|
|
6
|
+
|
|
3
7
|
## [0.1.0] - 2022-01-16
|
|
4
8
|
|
|
5
9
|
- Initial release. This version of the gem has no functionality whatsoever and is intended solely as a deployment test.
|
data/Gemfile.lock
CHANGED
|
@@ -1,17 +1,27 @@
|
|
|
1
1
|
PATH
|
|
2
2
|
remote: .
|
|
3
3
|
specs:
|
|
4
|
-
porridge (0.
|
|
4
|
+
porridge (0.2.0)
|
|
5
|
+
activesupport (~> 5.0)
|
|
5
6
|
|
|
6
7
|
GEM
|
|
7
8
|
remote: https://rubygems.org/
|
|
8
9
|
specs:
|
|
10
|
+
activesupport (5.2.5)
|
|
11
|
+
concurrent-ruby (~> 1.0, >= 1.0.2)
|
|
12
|
+
i18n (>= 0.7, < 2)
|
|
13
|
+
minitest (~> 5.1)
|
|
14
|
+
tzinfo (~> 1.1)
|
|
9
15
|
ast (2.4.2)
|
|
10
16
|
byebug (11.1.3)
|
|
11
17
|
codecov (0.6.0)
|
|
12
18
|
simplecov (>= 0.15, < 0.22)
|
|
19
|
+
concurrent-ruby (1.1.9)
|
|
13
20
|
diff-lcs (1.5.0)
|
|
14
21
|
docile (1.4.0)
|
|
22
|
+
i18n (1.8.11)
|
|
23
|
+
concurrent-ruby (~> 1.0)
|
|
24
|
+
minitest (5.15.0)
|
|
15
25
|
parallel (1.21.0)
|
|
16
26
|
parser (3.1.0.0)
|
|
17
27
|
ast (~> 2.4.1)
|
|
@@ -54,6 +64,9 @@ GEM
|
|
|
54
64
|
simplecov_json_formatter (~> 0.1)
|
|
55
65
|
simplecov-html (0.12.3)
|
|
56
66
|
simplecov_json_formatter (0.1.3)
|
|
67
|
+
thread_safe (0.3.6)
|
|
68
|
+
tzinfo (1.2.9)
|
|
69
|
+
thread_safe (~> 0.1)
|
|
57
70
|
unicode-display_width (2.1.0)
|
|
58
71
|
|
|
59
72
|
PLATFORMS
|
data/README.md
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# Porridge
|
|
2
|
-
|
|
2
|
+
[](https://badge.fury.io/rb/porridge)
|
|
3
|
+

|
|
3
4
|
[](https://codeclimate.com/github/jacoblockard99/porridge/maintainability)
|
|
4
5
|
[](https://codecov.io/gh/jacoblockard99/porridge)
|
|
5
6
|
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {ArraySerializer} is a serializer that wraps another serializer, calling it for every element of the input array,
|
|
5
|
+
# if an array was given, or simply passing it the input if not.
|
|
6
|
+
class ArraySerializer < Serializer
|
|
7
|
+
# Creates a new instance of {ArraySerializer} with the given base serializer.
|
|
8
|
+
# @param base [Serializer, #call] the base serializer to call for any input, or all elements of that input if the
|
|
9
|
+
# input is an array.
|
|
10
|
+
# @raise [InvalidSerializerError] if the given base serializer is not a valid serializer.
|
|
11
|
+
def initialize(base)
|
|
12
|
+
Serializer.ensure_valid!(base)
|
|
13
|
+
@base = base
|
|
14
|
+
super()
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Serializes the given object, which may be an array, for the given input with the given options. If the object
|
|
18
|
+
# is an array (according to {#array?}), the base serializer {#base} will be called for each element, and an array
|
|
19
|
+
# with each result will be returned. If the object is not an array, will simply delegate to {#base}.
|
|
20
|
+
#
|
|
21
|
+
# The given object and options will be given to the base serializer for every element. Note that the options are
|
|
22
|
+
# *not* cloned or duplicated. Therefore <b>the base serializer must not mutate the options object</b> or else
|
|
23
|
+
# the other invocations will also receive the mutated version.
|
|
24
|
+
#
|
|
25
|
+
# @param object_or_objects [Object, Array<Object>] the object or array of objects for which to transform the input.
|
|
26
|
+
# @param input the object being transformed, typically either a hash or an array.
|
|
27
|
+
# @param options [Hash] a hash of "options," which may be application specific.
|
|
28
|
+
# @return [Object, Array<Object>] the transformed output if the object was not an array, or an array of all
|
|
29
|
+
# transformed outputs if the object was an array.
|
|
30
|
+
def call(object_or_objects, input, options)
|
|
31
|
+
if array?(object_or_objects)
|
|
32
|
+
object_or_objects.map { |object| base.call(object, input, options) }
|
|
33
|
+
else
|
|
34
|
+
base.call(object_or_objects, input, options)
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
protected
|
|
39
|
+
|
|
40
|
+
# Determines whether the given object is an array for the purposes of this {ArraySerializer} instance. The default
|
|
41
|
+
# implementation simple checks to see if the object implements the +map+ method. You may override this method to
|
|
42
|
+
# change the default behavior, if, for example, you have a non-array that implements +map+.
|
|
43
|
+
# @param object the object to check.
|
|
44
|
+
# @return [Boolean] +true+ if the given object functions like an array; +false+ if otherwise.
|
|
45
|
+
def array?(object)
|
|
46
|
+
object.respond_to? :map
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
private
|
|
50
|
+
|
|
51
|
+
# The base serializer, which will be called for the object, or each object, if an array is given.
|
|
52
|
+
# @return [Serializer, #call]
|
|
53
|
+
attr_reader :base
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {ChainSerializer} is a serializer that chains multiple other serializers together by passing the output of the
|
|
5
|
+
# first one as the input of the second, and the output of the second as the input of the third, and so on.
|
|
6
|
+
class ChainSerializer < Serializer
|
|
7
|
+
# Creates a new instance of {ChainSerializer} with the given serializers to chain.
|
|
8
|
+
# @param serializers [Array<Serializer,#call>] the splatted array of serializers to chain.
|
|
9
|
+
# @raise [InvalidSerializerError] if any of the given serializers are not valid serializers.
|
|
10
|
+
def initialize(*serializers)
|
|
11
|
+
super()
|
|
12
|
+
Serializer.ensure_valid!(*serializers)
|
|
13
|
+
@serializers = serializers
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
# Transforms the given input for the given object with the given options by chaining each serializer (contained in
|
|
17
|
+
# {#serializers}). The provided input will be given to the first serializer, whose output will be given to the next
|
|
18
|
+
# serializer, and so on for each serializer.
|
|
19
|
+
#
|
|
20
|
+
# The given object and options will be given to all the provided serializers. Note that the options are *not*
|
|
21
|
+
# cloned or duplicated. Therefore <b>none of the serializers should mutate the options object</b> or else
|
|
22
|
+
# all the other serializers will also receive the mutated version.
|
|
23
|
+
#
|
|
24
|
+
# @param object the object for which to transform the input.
|
|
25
|
+
# @param input the object being transformed, typically either a hash or an array.
|
|
26
|
+
# @param options [Hash] a hash of "options," which may be application specific.
|
|
27
|
+
# @return the transformed output, typically either a hash or an array, as returned from the final chained
|
|
28
|
+
# serializer.
|
|
29
|
+
def call(object, input, options)
|
|
30
|
+
output = input
|
|
31
|
+
serializers.each { |serializer| output = serializer.call(object, output, options) }
|
|
32
|
+
output
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# The array of chained serializers.
|
|
38
|
+
# @return [Array<Serializer, #call>]
|
|
39
|
+
attr_reader :serializers
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {Extractor} is the nominal base class for all porridge value extractors.
|
|
5
|
+
#
|
|
6
|
+
# A (value) extractor is an object that is capable of retrieving a value from an object, given a set of "options",
|
|
7
|
+
# which may be application-specific. You are encouraged, but not required, to have your extractors derive from this
|
|
8
|
+
# class. Currently, any object that implements a +#call+ method is a valid extractor.
|
|
9
|
+
class Extractor
|
|
10
|
+
# Determines whether the given object is a valid porridge extractor. Currently, any object that responds to the
|
|
11
|
+
# +#call+ method is valid.
|
|
12
|
+
# @param object the object to check.
|
|
13
|
+
# @return [Boolean] +true+ if the object is a valid extractor; +false+ otherwise.
|
|
14
|
+
def self.valid?(object)
|
|
15
|
+
object.respond_to? :call
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Ensures that all the provided objects are valid extractors, raising {InvalidExtractorError} if not.
|
|
19
|
+
# @param objects [Array] the splatted array of objects to validate.
|
|
20
|
+
# @return [Boolean] +true+ if all the objects were valid; raises an error otherwise.
|
|
21
|
+
# @raise [InvalidExtractorError] if any of the provided objects are not valid extractors.
|
|
22
|
+
def self.ensure_valid!(*objects)
|
|
23
|
+
objects.each { |object| raise InvalidExtractorError unless valid?(object) }
|
|
24
|
+
true
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Should extract a value from the given object with the given options. Subclasses should override this method.
|
|
28
|
+
# @param object the object from which to retrieve the value.
|
|
29
|
+
# @param options [Hash] a hash of "options," which may be application-specific.
|
|
30
|
+
# @return the extracted value.
|
|
31
|
+
def call(object, options); end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {Factory} is a class that is capable of instantiating various porridge serializers and extractors. All extractor-
|
|
5
|
+
# creation methods are suffixed with +_extractor+, all serializer-creation methods are suffixed with +_serializer+,
|
|
6
|
+
# and all field serializer-creation methods are suffixed with +_field_serializer+.
|
|
7
|
+
#
|
|
8
|
+
# You may subclass this class if you wish to change its behavior. For example, if you wished to substitute your
|
|
9
|
+
# own "from name" extractor, that accesses a hash value rather than sends a method call, for the default one:
|
|
10
|
+
#
|
|
11
|
+
# class CustomPorridgeFactory < Porridge::Factory
|
|
12
|
+
# def from_name_extractor(name)
|
|
13
|
+
# extractor HashValueExtractor.new(name)
|
|
14
|
+
# end
|
|
15
|
+
# end
|
|
16
|
+
#
|
|
17
|
+
# {#from_name_field_serializer}, {#attribute_extractor}, and {#attribute_field_serializer} would then be automatically
|
|
18
|
+
# updated in the process, along with any other methods that depend on the aforementioned ones.
|
|
19
|
+
#
|
|
20
|
+
# This method is rarely used directly. Typically, it is used in conjunction with a {SerializerDefiner} and/or
|
|
21
|
+
# {SerializerDefinition} instance.
|
|
22
|
+
class Factory
|
|
23
|
+
def extractor(base)
|
|
24
|
+
return nil if base.nil?
|
|
25
|
+
|
|
26
|
+
Extractor.ensure_valid!(base)
|
|
27
|
+
base
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def from_name_extractor(name)
|
|
31
|
+
extractor SendExtractor.new(name)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def custom_extractor(callback)
|
|
35
|
+
extractor callback
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def association_extractor(serializer:, extractor: nil, extraction_name: nil, callback: nil, &block)
|
|
39
|
+
extractor SerializingExtractor.new(
|
|
40
|
+
extractor || custom_extractor(callback) || custom_extractor(block) || from_name_extractor(extraction_name),
|
|
41
|
+
serializer
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
alias belongs_to_extractor association_extractor
|
|
45
|
+
alias has_many_extractor association_extractor
|
|
46
|
+
|
|
47
|
+
def serializer(base)
|
|
48
|
+
return nil if base.nil?
|
|
49
|
+
|
|
50
|
+
Serializer.ensure_valid!(base)
|
|
51
|
+
base
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def chain_serializer(*bases)
|
|
55
|
+
serializer ChainSerializer.new(*bases)
|
|
56
|
+
end
|
|
57
|
+
alias serializers chain_serializer
|
|
58
|
+
|
|
59
|
+
def for_extracted_serializer(serializer, extractor)
|
|
60
|
+
serializer SerializerForExtracted.new(serializer, extractor)
|
|
61
|
+
end
|
|
62
|
+
alias serializer_for_extracted for_extracted_serializer
|
|
63
|
+
|
|
64
|
+
def field_serializer(name, extractor)
|
|
65
|
+
serializer FieldSerializer.new(name, extractor)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def attribute_field_serializer(name, callback = nil, extraction_name: nil, &block)
|
|
69
|
+
extractor = custom_extractor(callback || block) || from_name_extractor(extraction_name || name)
|
|
70
|
+
field_serializer(name, extractor)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def association_field_serializer(name, options = {}, &block)
|
|
74
|
+
options[:extraction_name] ||= name
|
|
75
|
+
field_serializer(name, association_extractor(**options, &block))
|
|
76
|
+
end
|
|
77
|
+
alias belongs_to_field_serializer association_field_serializer
|
|
78
|
+
alias has_many_field_serializer association_field_serializer
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {FieldPolicy} is the nominal base class for all field policy classes.
|
|
5
|
+
#
|
|
6
|
+
# A field policy is an object that is capable of determining whether a certain "field" is allowed in a given
|
|
7
|
+
# context. Currently, it is primarily used in {FieldSerializer} as the default method of determining whether a field
|
|
8
|
+
# is valid. You are encouraged, but not required, to have your own custom field policies derive from this class.
|
|
9
|
+
# Currently, any object that implements the +#allowed?+ method is a valid field policy.
|
|
10
|
+
class FieldPolicy
|
|
11
|
+
# Determines whether the given object is a valid porridge field policy. Currently, any object that responds to the
|
|
12
|
+
# +#allowed?+ method is valid.
|
|
13
|
+
# @param object the object to check.
|
|
14
|
+
# @return [Boolean] +true+ if the object is a valid field policy; +false+ otherwise.
|
|
15
|
+
def self.valid?(object)
|
|
16
|
+
object.respond_to? :allowed?
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# Ensures that all the provided objects are valid field policies, raising {InvalidFieldPolicyError} if not.
|
|
20
|
+
# @param objects [Array] the splatted array of objects to validate.
|
|
21
|
+
# @return [Boolean] +true+ if all the objects were valid; raises an error otherwise.
|
|
22
|
+
# @raise [InvalidFieldPolicyError] if any of the provided objects are not valid field policies.
|
|
23
|
+
def self.ensure_valid!(*objects)
|
|
24
|
+
objects.each { |object| raise InvalidFieldPolicyError unless valid?(object) }
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Determiners whether the field with the given name for the given object with the given options is currently
|
|
29
|
+
# allowed.
|
|
30
|
+
# @param _name the name of the field being validated.
|
|
31
|
+
# @param _object the object for which the field being validated is being generated.
|
|
32
|
+
# @param _options [Hash] the options with which the field being validated is being generated.
|
|
33
|
+
# @return [Boolean] +true+ if the indicated field is allowed; +false+ otherwise.
|
|
34
|
+
def allowed?(_name, _object, _options)
|
|
35
|
+
true
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {FieldSerializer} is a serializer that adds a "field" to a hash. It does so by using a predefined "field name" as
|
|
5
|
+
# the key and evaluates an {Extractor} for the object for the value.
|
|
6
|
+
#
|
|
7
|
+
# {FieldSerializer} is the most opinionated piece of the porridge framework. In particular, it adds to a
|
|
8
|
+
# +:field_hierarchy+ array in the options hash to keep track of nested fields. It also requires the use of a
|
|
9
|
+
# {FieldPolicy} object given in the options to determine whether a given field is allowed. Do not be afraid to
|
|
10
|
+
# subclass {FieldSerializer} or even create a new "field serializer" class altogether if you want to substantially
|
|
11
|
+
# change the way fields are implemented.
|
|
12
|
+
class FieldSerializer < Serializer
|
|
13
|
+
# Creates a new instance of {FieldSerializer} with the given field name and value extractor.
|
|
14
|
+
# @param name the name of the field; will be used as the key for the field in the hash.
|
|
15
|
+
# @param extractor [Extractor, #call] the value extractor to use to retrieve a value from the object, which will be
|
|
16
|
+
# used as the value for the field in the hash.
|
|
17
|
+
# @raise [InvalidExtractorError] if the provided extractor is not a valid extractor.
|
|
18
|
+
def initialize(name, extractor)
|
|
19
|
+
@name = name
|
|
20
|
+
@extractor = extractor
|
|
21
|
+
Extractor.ensure_valid!(extractor)
|
|
22
|
+
super()
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Serializes the given input hash for the given object with the given options by adding an element to the hash with
|
|
26
|
+
# a key that is equal to the field name ({#name}) and a value extracted from the object using the field extractor
|
|
27
|
+
# ({#extractor}).
|
|
28
|
+
# @param object the object for which to transform the input. The field value will be retrieved from this object
|
|
29
|
+
# using the extractor.
|
|
30
|
+
# @param hash [Hash] the input hash being transformed. A key-value pair will be added for the field.
|
|
31
|
+
# @param options [Hash] a hash of "options," which may be application specific.
|
|
32
|
+
# @option options [FieldPolicy, #allowed?] :field_policy the field policy to use to determine whether the field
|
|
33
|
+
# is currently allowed. This option *must* be provided.
|
|
34
|
+
# @return [Hash] the transformed hash.
|
|
35
|
+
# @raise [InvalidFieldPolicyError] if no field policy was provided or if the field policy was not a valid field
|
|
36
|
+
# policy object.
|
|
37
|
+
def call(object, hash, options)
|
|
38
|
+
if allowed?(object, options)
|
|
39
|
+
options = options.dup
|
|
40
|
+
add_field_to_hierarchy!(options)
|
|
41
|
+
hash_with_field(object, hash, options)
|
|
42
|
+
else
|
|
43
|
+
hash
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
# Determines whether the given object/options, along with the current {#name} constitutes a valid field. Currently,
|
|
50
|
+
# this is done by simply delegating to the field policy which should have been provided as an option.
|
|
51
|
+
# @param object the object for which the field being validated is being implemented.
|
|
52
|
+
# @param options [Hash] the options for which the field being validated is being implemented.
|
|
53
|
+
# @return [Boolean] +true+ if the indicated field is valid; +false+ otherwise.
|
|
54
|
+
# @raise [InvalidFieldPolicyError] if no field policy was provided or if the field policy was not a valid field
|
|
55
|
+
# policy object.
|
|
56
|
+
def allowed?(object, options)
|
|
57
|
+
FieldPolicy.ensure_valid!(options[:field_policy])
|
|
58
|
+
options[:field_policy].allowed?(name, object, options.except(:field_policy))
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Safely adds the current {#name} to the +:field_hierarchy+ in the given options hash. While the options hash itself
|
|
62
|
+
# is mutated, the field hierarchy array is first duplicated, meaning the options hash must have first been
|
|
63
|
+
# duplicated for this to be safe.
|
|
64
|
+
# @param options_hash [Hash] the options hash to which the field should be added.
|
|
65
|
+
# @return [void]
|
|
66
|
+
def add_field_to_hierarchy!(options_hash)
|
|
67
|
+
options_hash[:field_hierarchy] ||= []
|
|
68
|
+
options_hash[:field_hierarchy] = options_hash[:field_hierarchy].dup
|
|
69
|
+
options_hash[:field_hierarchy] << name
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Creates a new hash from the given one with a field for the given object injected.
|
|
73
|
+
# @param object the object for which to inject the field. Will be passed to the extractor.
|
|
74
|
+
# @param hash [Hash] the hash into which to inject the field.
|
|
75
|
+
# @param options [Hash] the options for which to inject the field. Will be passed to the extractor.
|
|
76
|
+
# @return [Hash] the transformed hash.
|
|
77
|
+
def hash_with_field(object, hash, options)
|
|
78
|
+
hash.merge(name => extractor.call(object, options))
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# The name of the field being serialized by this {FieldSerializer}; used as the key for the field in the hash.
|
|
82
|
+
attr_reader :name
|
|
83
|
+
|
|
84
|
+
# The value extractor to use to retrieve a value from the object for which serialization is occurring.
|
|
85
|
+
# @return [Extractor, #call]
|
|
86
|
+
attr_reader :extractor
|
|
87
|
+
end
|
|
88
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/hash/keys'
|
|
5
|
+
|
|
6
|
+
module Porridge
|
|
7
|
+
# {KeyNormalizingSerializer} is a serializer that wraps another serializer and recursively normalizes the keys of the
|
|
8
|
+
# resulting hash to either strings or symbols.
|
|
9
|
+
class KeyNormalizingSerializer < Serializer
|
|
10
|
+
# Creates a new instance of {KeyNormalizingSerializer} with the given base serializer and key type.
|
|
11
|
+
# @param base [Serializer, #call] the base serializer to wrap. Note that the output of the base serializer *must*
|
|
12
|
+
# be a hash.
|
|
13
|
+
# @param key_type [Symbol] the type that the keys should be normalized to. Both +:string+ and +:symbol+ are
|
|
14
|
+
# supported.
|
|
15
|
+
# @raise [InvalidSerializerError] if the given base serializer is not a valid serializer.
|
|
16
|
+
def initialize(base, key_type: :string)
|
|
17
|
+
Serializer.ensure_valid!(base)
|
|
18
|
+
@base = base
|
|
19
|
+
@key_type = key_type
|
|
20
|
+
super()
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Serializes the given input for the given object with the given options by delegating to the base serializer
|
|
24
|
+
# ({#base}) and recursively transforming the keys of the resulting hash to the appropriate type ({#key_type}).
|
|
25
|
+
# Note that the output of the base serializer *must* be a hash.
|
|
26
|
+
# @param object the object for which to transform the input.
|
|
27
|
+
# @param input the object being transformed, typically either a hash or an array.
|
|
28
|
+
# @param options [Hash] a hash of "options," which may be application specific.
|
|
29
|
+
# @return [Hash] the hash returned from the base serializer, normalized.
|
|
30
|
+
def call(object, input, options)
|
|
31
|
+
normalize_keys(base.call(object, input, options))
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Normalizes the keys of the given hash according to the {#key_type}. Uses ActiveSupport methods to accomplish this.
|
|
37
|
+
# @param hash [Hash] the hash to normalize.
|
|
38
|
+
# @return [Hash] the normalized hash.
|
|
39
|
+
def normalize_keys(hash)
|
|
40
|
+
key_type == :symbol ? hash.deep_symbolize_keys : hash.deep_stringify_keys
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# The base serializer whose output hash will be normalized
|
|
44
|
+
# @return [Serializer, #call]
|
|
45
|
+
attr_reader :base
|
|
46
|
+
|
|
47
|
+
# The key type that the hash should be normalized to.
|
|
48
|
+
# @return [Symbol] either +:string+ or +:symbol+.
|
|
49
|
+
attr_reader :key_type
|
|
50
|
+
end
|
|
51
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {SendExtractor} is an extractor that retrieves a value from an object by simply calling a predefined method on it.
|
|
5
|
+
class SendExtractor < Extractor
|
|
6
|
+
# Creates a new instance of {SendExtractor} with the given method name.
|
|
7
|
+
# @param method_name [String, Symbol] the name of the method to call when extracting the value.
|
|
8
|
+
def initialize(method_name)
|
|
9
|
+
@method_name = method_name.to_s
|
|
10
|
+
super()
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# Extracts the value from the given object by sending the method name ({#method_name}) to it.
|
|
14
|
+
# @param object the object from which to retrieve the value.
|
|
15
|
+
# @param _options [Hash] a hash of "options," which may be application-specific. These options are ignored.
|
|
16
|
+
# @return the extracted value, as returned from the sent method.
|
|
17
|
+
def call(object, _options)
|
|
18
|
+
object.respond_to?(method_name) ? object.send(method_name) : nil
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# The name of the method to call when extracting the value.
|
|
24
|
+
# @return [String]
|
|
25
|
+
attr_reader :method_name
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {Serializer} is the nominal base class for all porridge serializers.
|
|
5
|
+
#
|
|
6
|
+
# A serializer is an object that arbitrarily transforms a given input for a given object with a given set of options.
|
|
7
|
+
# The input may be anything, but is typically either a hash or an array. In Rails applications, the object is often
|
|
8
|
+
# an ActiveRecord model. The options may be application-specific.
|
|
9
|
+
#
|
|
10
|
+
# Serializers are the heart and soul of the porridge gem and are typically layered with composition into a final
|
|
11
|
+
# serializer that is used to actually serialize an object into (typically) a hash or array. Thus, a serializer often
|
|
12
|
+
# simply wraps another serializer, transforming the object or options in some way.
|
|
13
|
+
#
|
|
14
|
+
# You are encouraged, but not required, to have all your serializers derive from this class. Currently, any object
|
|
15
|
+
# that implements the +#call+ method is a valid serializer.
|
|
16
|
+
class Serializer
|
|
17
|
+
# Determines whether the given object is a valid porridge serializer. Currently, any object that responds to the
|
|
18
|
+
# '#call' method is valid.
|
|
19
|
+
# @param object the object to check.
|
|
20
|
+
# @return [Boolean] +true+ if the object is a valid serializer; +false+ otherwise.
|
|
21
|
+
def self.valid?(object)
|
|
22
|
+
object.respond_to? :call
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Ensures that all the provided objects are valid serializers, raising {InvalidSerializerError} if not.
|
|
26
|
+
# @param objects [Array] the splatted array of objects to validate.
|
|
27
|
+
# @return [Boolean] +true+ if all the objects were valid; raises an error otherwise.
|
|
28
|
+
# @raise [InvalidSerializerError] if any of the provided objects are not valid serializers.
|
|
29
|
+
def self.ensure_valid!(*objects)
|
|
30
|
+
objects.each { |object| raise InvalidSerializerError unless valid?(object) }
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Should transforms the given input for the given object with the given options and return the desired output.
|
|
35
|
+
# @param _object the object for which to transform the input.
|
|
36
|
+
# @param input the object being transformed, typically either a hash or an array.
|
|
37
|
+
# @param _options [Hash] a hash of "options," which may be application specific.
|
|
38
|
+
# @return the transformed output, typically either a hash or an array.
|
|
39
|
+
def call(_object, input, _options)
|
|
40
|
+
input
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {SerializerDefiner} is a class that wraps a {Factory} and allows serializes to be easily defined with an elegant
|
|
5
|
+
# DSL.
|
|
6
|
+
class SerializerDefiner
|
|
7
|
+
FACTORY_PREFIX = 'create_'
|
|
8
|
+
SERIALIZER_SUFFIX = '_serializer'
|
|
9
|
+
FIELD_SERIALIZER_SUFFIX = '_field_serializer'
|
|
10
|
+
|
|
11
|
+
delegate :call, to: :defined_serializer
|
|
12
|
+
|
|
13
|
+
def initialize(factory = Factory.new)
|
|
14
|
+
@factory = factory
|
|
15
|
+
@serializers = []
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def method_missing(method_name, *args, &block)
|
|
19
|
+
method_name = method_name.to_s
|
|
20
|
+
return factory.send(method_name.delete_prefix(FACTORY_PREFIX), *args, &block) if create_method? method_name
|
|
21
|
+
|
|
22
|
+
if serializer_method? method_name
|
|
23
|
+
return add_serializer(factory.send(method_name + SERIALIZER_SUFFIX, *args, &block))
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if field_serializer_method? method_name
|
|
27
|
+
return add_serializer(factory.send(method_name + FIELD_SERIALIZER_SUFFIX, *args, &block))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
super(method_name.to_sym, *args, &block)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
34
|
+
method_name = method_name.to_s
|
|
35
|
+
super(method_name.to_sym, include_private) ||
|
|
36
|
+
create_method?(method_name) ||
|
|
37
|
+
serializer_method?(method_name) ||
|
|
38
|
+
field_serializer_method?(method_name)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def defined_serializer
|
|
42
|
+
factory.serializers(*added_serializers)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def added_serializers
|
|
46
|
+
@serializers
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def serializer(...)
|
|
50
|
+
add_serializer(factory.serializer(...))
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
def create_method?(method_name)
|
|
56
|
+
method_name.start_with?(FACTORY_PREFIX) && factory.respond_to?(method_name.delete_prefix(FACTORY_PREFIX))
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def serializer_method?(method_name)
|
|
60
|
+
factory.respond_to?(method_name + SERIALIZER_SUFFIX)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def field_serializer_method?(method_name)
|
|
64
|
+
factory.respond_to?(method_name + FIELD_SERIALIZER_SUFFIX)
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def add_serializer(serializer)
|
|
68
|
+
@serializers << serializer
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
attr_reader :factory
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {SerializerDefinition} is a class that allows serializers to be defined as with a {SerializerDefiner}, but within
|
|
5
|
+
# a class. Simply subclass this class and use the same DSL within it.
|
|
6
|
+
class SerializerDefinition
|
|
7
|
+
class << self
|
|
8
|
+
attr_writer :definer
|
|
9
|
+
|
|
10
|
+
delegate_missing_to :definer
|
|
11
|
+
|
|
12
|
+
def inherited(subclass)
|
|
13
|
+
super
|
|
14
|
+
definer.added_serializers.each { |serializer| subclass.definer.serializer(serializer) }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def definer
|
|
18
|
+
@definer ||= create_definer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def create_definer
|
|
22
|
+
SerializerDefiner.new
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def reset!
|
|
26
|
+
@definer = nil
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {SerializerForExtracted} is a serializer that wraps another serializer and passes it an object that is extracted
|
|
5
|
+
# from the initial object using an {Extractor}.
|
|
6
|
+
class SerializerForExtracted < Serializer
|
|
7
|
+
# Creates a new instance of {SerializerForExtracted} with the given base serializer and extractor.
|
|
8
|
+
# @param base [Serializer, #call] the base serializer to wrap.
|
|
9
|
+
# @param extractor [Extractor, #call] the extractor to use to extract a value from the object before passing it
|
|
10
|
+
# to the base serializer.
|
|
11
|
+
# @raise [InvalidSerializerError] if the provided base serializer is not a valid serializer.
|
|
12
|
+
# @raise [InvalidExtractorError] if the provided extractor is not a valid extractor.
|
|
13
|
+
def initialize(base, extractor)
|
|
14
|
+
Serializer.ensure_valid!(base)
|
|
15
|
+
Extractor.ensure_valid!(extractor)
|
|
16
|
+
@base = base
|
|
17
|
+
@extractor = extractor
|
|
18
|
+
super()
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Serializes the given input for the given object with the given options by first extracted a value from the given
|
|
22
|
+
# object, then passing that value, along with the given input and options, to the base serializer ({#base}).
|
|
23
|
+
# @param object the object for which to transform the input. A value will be extracted and that value will be passed
|
|
24
|
+
# to the base serializer.
|
|
25
|
+
# @param input the object being transformed, typically either a hash or an array.
|
|
26
|
+
# @param options [Hash] a hash of "options," which may be application specific.
|
|
27
|
+
# @return the transformed output, typically either a hash or an array, as returned from the base serializer.
|
|
28
|
+
def call(object, input, options)
|
|
29
|
+
extracted_value = extractor.call(object, options)
|
|
30
|
+
base.call(extracted_value, input, options)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# The base serializer to wrap.
|
|
36
|
+
# @return [Serializer, #call]
|
|
37
|
+
attr_reader :base
|
|
38
|
+
|
|
39
|
+
# The extractor to use to extract a value from the object before passing it to the base serializer.
|
|
40
|
+
# @return [Extractor, #call]
|
|
41
|
+
attr_reader :extractor
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'active_support'
|
|
4
|
+
require 'active_support/core_ext/string/inflections'
|
|
5
|
+
|
|
6
|
+
module Porridge
|
|
7
|
+
# {SerializerWithRoot} is a serializer that wraps another serializer and adds a "root key" to the resulting hash.
|
|
8
|
+
class SerializerWithRoot < Serializer
|
|
9
|
+
# Creates a new instance of {SerializerWithRoot} with the given base serializer and, optionally, root key.
|
|
10
|
+
# @param base [Serializer, #call] the base serializer to wrap.
|
|
11
|
+
# @param root_key the "root" key to inject into the resulting hash. If +nil+, which is the default, the root key
|
|
12
|
+
# will be inferred from the object.
|
|
13
|
+
# @raise [InvalidSerializerError] if the provided base serializer is not a valid serializer.
|
|
14
|
+
def initialize(base, root_key: nil)
|
|
15
|
+
Serializer.ensure_valid!(base)
|
|
16
|
+
@base = base
|
|
17
|
+
@root_key = root_key
|
|
18
|
+
super()
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Serializes the given input for the given object with the given options by delegating to the base serializer
|
|
22
|
+
# ({#base}) and adding a root key to the resulting hash. Note that the output of the base serializer *must* be a
|
|
23
|
+
# hash.
|
|
24
|
+
#
|
|
25
|
+
# If the root key was not set manually, it will be inferred from the "underscored" class name of the object. If the
|
|
26
|
+
# object is an array (according to {#array?}), then the class name will be derived from the first object in the
|
|
27
|
+
# array, and will be pluralized. Be aware that retrieving the first element of the "array" may cause an SQL query to
|
|
28
|
+
# be performed if the "array" is a Rails relation.
|
|
29
|
+
#
|
|
30
|
+
# Note that an inferred root key is always a string. You may wish to use a {KeyNormalizingSerializer} if symbol
|
|
31
|
+
# keys are desired.
|
|
32
|
+
#
|
|
33
|
+
# @param object the object for which to transform the input. If no root key was set manually, it will be inferred
|
|
34
|
+
# from the object's class.
|
|
35
|
+
# @param input the object being transformed, typically either a hash or an array.
|
|
36
|
+
# @param options [Hash] a hash of "options," which may be application specific.
|
|
37
|
+
# @return [Hash] the hash returned from the base serializer, injected with a root key.
|
|
38
|
+
def call(object, input, options)
|
|
39
|
+
{ evaluate_root_key(object) => base.call(object, input, options) }
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
protected
|
|
43
|
+
|
|
44
|
+
# Determines whether the given object functions like an array for the purposes of this {SerializerWithRoot}. The
|
|
45
|
+
# default implementation checks to see whether the object implements both +#map+ and +#first+. You may override the
|
|
46
|
+
# default behavior by overriding this method. Note that if you override {ArraySerializer#like_array?} you will
|
|
47
|
+
# likely wish to override this method as well.
|
|
48
|
+
# @param object the object to check.
|
|
49
|
+
# @return [Boolean] +true+ if the given object is like an array; +false+ otherwise.
|
|
50
|
+
def array?(object)
|
|
51
|
+
object.respond_to?(:map) && object.respond_to?(:first)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
private
|
|
55
|
+
|
|
56
|
+
# Gets a root key for the given object by either returning {#root_key}, or returning a singular/plural version
|
|
57
|
+
# of the {#base_root_key}, depending on whether the object is an array.
|
|
58
|
+
# @param object the object for which to get a root key.
|
|
59
|
+
# @return the resolved string root key.
|
|
60
|
+
def evaluate_root_key(object)
|
|
61
|
+
return root_key if root_key
|
|
62
|
+
|
|
63
|
+
array?(object) ? base_root_key(object).pluralize : base_root_key(object).singularize
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Gets the inferred base root key, without singularization or pluralization for the given object.
|
|
67
|
+
# @param object the object for which to get the base root key.
|
|
68
|
+
# @return [String] the resolved base root key.
|
|
69
|
+
def base_root_key(object)
|
|
70
|
+
representative_sample(object).class.name.underscore.to_s
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# Gets a "representative" sample from the given object. In practice, this means either returning the object itself,
|
|
74
|
+
# or, if the object is an array-like structure, returning the first element.
|
|
75
|
+
def representative_sample(object)
|
|
76
|
+
array?(object) ? object.first : object
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# The base serializer to wrap.
|
|
80
|
+
# @return [Serializer, #call]
|
|
81
|
+
attr_reader :base
|
|
82
|
+
|
|
83
|
+
# The explicit root key; if +nil+, will be inferred.
|
|
84
|
+
attr_reader :root_key
|
|
85
|
+
end
|
|
86
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {SerializingExtractor} is an extractor that wraps another extractor and serializes for its output using a provided
|
|
5
|
+
# serializer. Note that {SerializingExtractor} passes the output of the base extractor as the *object* for the
|
|
6
|
+
# serializer, not the input.
|
|
7
|
+
class SerializingExtractor < Extractor
|
|
8
|
+
# Creates a new instance of {SerializingExtractor} with the given base extractor and serializer.
|
|
9
|
+
# @param base [Extractor, #call] the extractor to wrap, whose output will be serialized for before being returned.
|
|
10
|
+
# @param serializer [Serializer, #call] the serializer to use to transform the output of the extractor.
|
|
11
|
+
# @raise [InvalidExtractorError] if the provided base extractor was not a valid extractor.
|
|
12
|
+
# @raise [InvalidSerializerError] if the provided serializer was not a valid serializer.
|
|
13
|
+
def initialize(base, serializer)
|
|
14
|
+
Extractor.ensure_valid!(base)
|
|
15
|
+
Serializer.ensure_valid!(serializer)
|
|
16
|
+
@base = base
|
|
17
|
+
@serializer = serializer
|
|
18
|
+
super()
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Extracts a value from the given object for the given options by:
|
|
22
|
+
# 1. Using the base extractor ({#extractor}) to extract a value from the object with the given options; and
|
|
23
|
+
# 2. Passing that value as the object to {#serializer#call}. A blank hash is given as the input, and the
|
|
24
|
+
# given options are passed along.
|
|
25
|
+
# @param object the object from which to retrieve the value.
|
|
26
|
+
# @param options [Hash] a hash of "options," which may be application-specific.
|
|
27
|
+
# @return the extracted value.
|
|
28
|
+
def call(object, options)
|
|
29
|
+
serializer.call(base.call(object, options), {}, options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# The base extractor.
|
|
35
|
+
# @return [Extractor, #call]
|
|
36
|
+
attr_reader :base
|
|
37
|
+
|
|
38
|
+
# The serializer to use to serialize for the output of the base extractor.
|
|
39
|
+
# @return [Serializer, #call]
|
|
40
|
+
attr_reader :serializer
|
|
41
|
+
end
|
|
42
|
+
end
|
data/lib/porridge/version.rb
CHANGED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Porridge
|
|
4
|
+
# {WhitelistFieldPolicy} is a field policy that uses a nested whitelist of field names to determine which fields are
|
|
5
|
+
# valid.
|
|
6
|
+
class WhitelistFieldPolicy < FieldPolicy
|
|
7
|
+
# Creates a new instance of {WhitelistFieldPolicy} with the given whitelist.
|
|
8
|
+
# @param whitelist [Hash] the nested whitelist hash of allowed field names.
|
|
9
|
+
def initialize(whitelist)
|
|
10
|
+
@whitelist = whitelist
|
|
11
|
+
super()
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Determiners whether the field with the given name with the given options is currently allowed by checking the
|
|
15
|
+
# field hierarchy, which must be contained in +options[:field_hierarchy] against the whitelist.
|
|
16
|
+
# @param name the name of the field being validated.
|
|
17
|
+
# @param _object the object for which the field being validated is being generated.
|
|
18
|
+
# @param options [Hash] the options with which the field being validated is being generated.
|
|
19
|
+
# @return [Boolean] +true+ if the indicated field is allowed; +false+ otherwise.
|
|
20
|
+
def allowed?(name, _object, options)
|
|
21
|
+
field_hierarchy = options[:field_hierarchy] || []
|
|
22
|
+
_allowed?([*field_hierarchy, name], whitelist)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
# Determines whether the given object functions as a hash for the purposes of this {WhitelistFieldPolicy} instance.
|
|
28
|
+
# You may override this method if desired, but hashes must at least respond to +#[]+.
|
|
29
|
+
# @param input the input object to check.
|
|
30
|
+
# @return [Boolean] +true+ if the given object is like a hash; +false+ otherwise.
|
|
31
|
+
def hash?(input)
|
|
32
|
+
input.is_a? Hash
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
# @overload _allowed?(field_hierarchy, whitelist)
|
|
38
|
+
# Recursively traverses the given field hierarchy and determines whether the field indicated by the hierarchy is
|
|
39
|
+
# allowed for the given whitelist.
|
|
40
|
+
# @param field_hierarchy [Array] the field hierarchy to validate.
|
|
41
|
+
# @param whitelist [Hash] the nested whitelist hash of field names.
|
|
42
|
+
# @overload _allowed?(field_hierarchy, whitelist, level)
|
|
43
|
+
# Recursively traverses the given field hierarchy and determines whether the field indicated by the hierarchy is
|
|
44
|
+
# allowed for the given whitelist, starting from the specified level.
|
|
45
|
+
# @param field_hierarchy [Array] the field hierarchy to validate.
|
|
46
|
+
# @param whitelist [Hash] the nested whitelist hash of field names.
|
|
47
|
+
# @param level [Integer] the current level of the hierarchy being checked.
|
|
48
|
+
def _allowed?(field_hierarchy, whitelist, level = 0)
|
|
49
|
+
# If the level is equal to the field hierarchy length, then we've reached the end. Immediately return the
|
|
50
|
+
# truthiness of whitelist, which is now equal to the final resolved value referenced by the field hierarchy.
|
|
51
|
+
return !!whitelist if level >= field_hierarchy.count
|
|
52
|
+
|
|
53
|
+
# If the current whitelist is not a hash, then the field hierarchy is deeper than the whitelist.
|
|
54
|
+
# As an example, take this whitelist:
|
|
55
|
+
# { users: true }
|
|
56
|
+
# And this field hierarchy:
|
|
57
|
+
# [:user, :id]
|
|
58
|
+
# One interpretation of this is that since 'users' is true, all fields should be allowed. We take the opposite
|
|
59
|
+
# approach and say that no attributes have been explicitly defined.
|
|
60
|
+
# Therefore immediately return false.
|
|
61
|
+
return false unless hash?(whitelist)
|
|
62
|
+
|
|
63
|
+
_allowed?(field_hierarchy, whitelist[field_hierarchy[level]], level + 1)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# The nested whitelist hash of field names.
|
|
67
|
+
# @return [Hash]
|
|
68
|
+
attr_reader :whitelist
|
|
69
|
+
end
|
|
70
|
+
end
|
data/lib/porridge.rb
CHANGED
|
@@ -1,8 +1,25 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require_relative 'porridge/version'
|
|
4
|
+
require_relative 'porridge/extractor'
|
|
5
|
+
require_relative 'porridge/send_extractor'
|
|
6
|
+
require_relative 'porridge/serializer'
|
|
7
|
+
require_relative 'porridge/chain_serializer'
|
|
8
|
+
require_relative 'porridge/array_serializer'
|
|
9
|
+
require_relative 'porridge/key_normalizing_serializer'
|
|
10
|
+
require_relative 'porridge/serializer_for_extracted'
|
|
11
|
+
require_relative 'porridge/serializer_with_root'
|
|
12
|
+
require_relative 'porridge/error'
|
|
13
|
+
require_relative 'porridge/invalid_serializer_error'
|
|
14
|
+
require_relative 'porridge/invalid_extractor_error'
|
|
15
|
+
require_relative 'porridge/invalid_field_policy_error'
|
|
16
|
+
require_relative 'porridge/field_policy'
|
|
17
|
+
require_relative 'porridge/field_serializer'
|
|
18
|
+
require_relative 'porridge/serializing_extractor'
|
|
19
|
+
require_relative 'porridge/whitelist_field_policy'
|
|
20
|
+
require_relative 'porridge/factory'
|
|
21
|
+
require_relative 'porridge/serializer_definer'
|
|
22
|
+
require_relative 'porridge/serializer_definition'
|
|
4
23
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
# Your code goes here...
|
|
8
|
-
end
|
|
24
|
+
# {Porridge} is the root namespace for all classes in the +porridge+ gem.
|
|
25
|
+
module Porridge; end
|
data/porridge.gemspec
CHANGED
|
@@ -42,6 +42,8 @@ Gem::Specification.new do |spec|
|
|
|
42
42
|
# Uncomment to register a new dependency of your gem
|
|
43
43
|
# spec.add_dependency "example-gem", "~> 1.0"
|
|
44
44
|
|
|
45
|
+
spec.add_dependency 'activesupport', '~> 5.0'
|
|
46
|
+
|
|
45
47
|
# For more information and examples about making a new gem, checkout our
|
|
46
48
|
# guide at: https://bundler.io/guides/creating_gem.html
|
|
47
49
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: porridge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jacob
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2022-01-
|
|
11
|
+
date: 2022-01-19 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: byebug
|
|
@@ -108,6 +108,20 @@ dependencies:
|
|
|
108
108
|
- - "~>"
|
|
109
109
|
- !ruby/object:Gem::Version
|
|
110
110
|
version: '0.21'
|
|
111
|
+
- !ruby/object:Gem::Dependency
|
|
112
|
+
name: activesupport
|
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
|
114
|
+
requirements:
|
|
115
|
+
- - "~>"
|
|
116
|
+
- !ruby/object:Gem::Version
|
|
117
|
+
version: '5.0'
|
|
118
|
+
type: :runtime
|
|
119
|
+
prerelease: false
|
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
121
|
+
requirements:
|
|
122
|
+
- - "~>"
|
|
123
|
+
- !ruby/object:Gem::Version
|
|
124
|
+
version: '5.0'
|
|
111
125
|
description: |
|
|
112
126
|
`porridge` is a plain Ruby gem that takes a flexible, object-oriented approach to serialization. `porridge`
|
|
113
127
|
transforms objects into ruby hashes and arrays, which can then be serialized with other libraries.
|
|
@@ -120,12 +134,15 @@ files:
|
|
|
120
134
|
- ".github/workflows/build.yml"
|
|
121
135
|
- ".gitignore"
|
|
122
136
|
- ".idea/.gitignore"
|
|
137
|
+
- ".idea/fileTemplates/includes/Ruby File Header.rb"
|
|
138
|
+
- ".idea/fileTemplates/internal/RSpec.rb"
|
|
123
139
|
- ".idea/misc.xml"
|
|
124
140
|
- ".idea/modules.xml"
|
|
125
141
|
- ".idea/porridge.iml"
|
|
126
142
|
- ".idea/vcs.xml"
|
|
127
143
|
- ".rspec"
|
|
128
144
|
- ".rubocop.yml"
|
|
145
|
+
- ".yardopts"
|
|
129
146
|
- CHANGELOG.md
|
|
130
147
|
- Gemfile
|
|
131
148
|
- Gemfile.lock
|
|
@@ -135,7 +152,26 @@ files:
|
|
|
135
152
|
- bin/console
|
|
136
153
|
- bin/setup
|
|
137
154
|
- lib/porridge.rb
|
|
155
|
+
- lib/porridge/array_serializer.rb
|
|
156
|
+
- lib/porridge/chain_serializer.rb
|
|
157
|
+
- lib/porridge/error.rb
|
|
158
|
+
- lib/porridge/extractor.rb
|
|
159
|
+
- lib/porridge/factory.rb
|
|
160
|
+
- lib/porridge/field_policy.rb
|
|
161
|
+
- lib/porridge/field_serializer.rb
|
|
162
|
+
- lib/porridge/invalid_extractor_error.rb
|
|
163
|
+
- lib/porridge/invalid_field_policy_error.rb
|
|
164
|
+
- lib/porridge/invalid_serializer_error.rb
|
|
165
|
+
- lib/porridge/key_normalizing_serializer.rb
|
|
166
|
+
- lib/porridge/send_extractor.rb
|
|
167
|
+
- lib/porridge/serializer.rb
|
|
168
|
+
- lib/porridge/serializer_definer.rb
|
|
169
|
+
- lib/porridge/serializer_definition.rb
|
|
170
|
+
- lib/porridge/serializer_for_extracted.rb
|
|
171
|
+
- lib/porridge/serializer_with_root.rb
|
|
172
|
+
- lib/porridge/serializing_extractor.rb
|
|
138
173
|
- lib/porridge/version.rb
|
|
174
|
+
- lib/porridge/whitelist_field_policy.rb
|
|
139
175
|
- porridge.gemspec
|
|
140
176
|
homepage: https://github.com/jacoblockard99/porridge
|
|
141
177
|
licenses:
|