porridge 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Gem Version](https://badge.fury.io/rb/porridge.svg)](https://badge.fury.io/rb/porridge)
|
3
|
+
![Build](https://github.com/jacoblockard99/porridge/actions/workflows/build.yml/badge.svg)
|
3
4
|
[![Maintainability](https://api.codeclimate.com/v1/badges/9c3a8a230097bac612e3/maintainability)](https://codeclimate.com/github/jacoblockard99/porridge/maintainability)
|
4
5
|
[![codecov](https://codecov.io/gh/jacoblockard99/porridge/branch/master/graph/badge.svg?token=V9GxyepasN)](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:
|