cinnamon_serial 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 82d270da4eb17b6f19b8bc07991f497001b624ff
4
+ data.tar.gz: e6066491403152ead4a08d7523c9a867cfb5de82
5
+ SHA512:
6
+ metadata.gz: 9f23a5d532c75e3dc676a947fb27abd1497601f269376a5c65a775cb9128edecad6ae803f28e2bb617a39e33fedb51da8ae7d6366674c9e359a465661d37639c
7
+ data.tar.gz: 1cf6043f141a31c2021ee2168bcb032f57990ce44e05a5bd9be9935afc34a1e72eb548bee619c8c344cd87c4062579e0c68243dfe277bf84ddaaa19b4525672e
data/.editorconfig ADDED
@@ -0,0 +1,8 @@
1
+ # See http://editorconfig.org/
2
+
3
+ [*]
4
+ trim_trailing_whitespace = true
5
+ indent_style = space
6
+ indent_size = 2
7
+ insert_final_newline = true
8
+ end_of_line = lf
data/.gitignore ADDED
@@ -0,0 +1,2 @@
1
+ .DS_Store
2
+ *.gem
data/.rubocop.yml ADDED
@@ -0,0 +1,7 @@
1
+ inherit_from: .rubocop_todo.yml
2
+
3
+ Metrics/LineLength:
4
+ Max: 100
5
+
6
+ Metrics/MethodLength:
7
+ Max: 20
data/.rubocop_todo.yml ADDED
@@ -0,0 +1,56 @@
1
+ # This configuration was generated by
2
+ # `rubocop --auto-gen-config`
3
+ # on 2018-10-03 13:08:39 -0500 using RuboCop version 0.59.2.
4
+ # The point is for the user to remove these configuration records
5
+ # one by one as the offenses are removed from the code base.
6
+ # Note that changes in the inspected code, or installation of new
7
+ # versions of RuboCop, may require this file to be generated again.
8
+
9
+ # Offense count: 2
10
+ # Cop supports --auto-correct.
11
+ # Configuration parameters: EnforcedStyle.
12
+ # SupportedStyles: empty_lines, empty_lines_except_namespace, empty_lines_special, no_empty_lines, beginning_only, ending_only
13
+ Layout/EmptyLinesAroundClassBody:
14
+ Exclude:
15
+ - 'spec/examples.rb'
16
+
17
+ # Offense count: 4
18
+ Lint/BooleanSymbol:
19
+ Exclude:
20
+ - 'lib/cinnamon_serial/resolver.rb'
21
+ - 'spec/examples.rb'
22
+
23
+ # Offense count: 4
24
+ Metrics/AbcSize:
25
+ Max: 26
26
+
27
+ # Offense count: 2
28
+ # Configuration parameters: CountComments, ExcludedMethods.
29
+ # ExcludedMethods: refine
30
+ Metrics/BlockLength:
31
+ Exclude:
32
+ - 'spec/base_spec.rb'
33
+ - 'spec/dsl_spec.rb'
34
+
35
+ # Offense count: 1
36
+ # Configuration parameters: CountComments.
37
+ Metrics/ClassLength:
38
+ Max: 111
39
+
40
+ # Offense count: 2
41
+ Metrics/CyclomaticComplexity:
42
+ Max: 15
43
+
44
+ # Offense count: 1
45
+ # Configuration parameters: CountKeywordArgs.
46
+ Metrics/ParameterLists:
47
+ Max: 8
48
+
49
+ # Offense count: 1
50
+ Metrics/PerceivedComplexity:
51
+ Max: 16
52
+
53
+ # Offense count: 1
54
+ Style/DoubleNegation:
55
+ Exclude:
56
+ - 'lib/cinnamon_serial/formatting.rb'
data/.ruby-version ADDED
@@ -0,0 +1 @@
1
+ 2.3.7
data/.travis.yml ADDED
@@ -0,0 +1,8 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.3.1
4
+ - 2.3.7
5
+ cache: bundler
6
+ script:
7
+ - bundle exec rubocop
8
+ - bundle exec rspec
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,56 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cinnamon_serial (1.0.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ ast (2.4.0)
10
+ coderay (1.1.2)
11
+ diff-lcs (1.3)
12
+ jaro_winkler (1.5.1)
13
+ method_source (0.9.0)
14
+ parallel (1.12.1)
15
+ parser (2.5.1.2)
16
+ ast (~> 2.4.0)
17
+ powerpack (0.1.2)
18
+ pry (0.11.3)
19
+ coderay (~> 1.1.0)
20
+ method_source (~> 0.9.0)
21
+ rainbow (3.0.0)
22
+ rspec (3.8.0)
23
+ rspec-core (~> 3.8.0)
24
+ rspec-expectations (~> 3.8.0)
25
+ rspec-mocks (~> 3.8.0)
26
+ rspec-core (3.8.0)
27
+ rspec-support (~> 3.8.0)
28
+ rspec-expectations (3.8.1)
29
+ diff-lcs (>= 1.2.0, < 2.0)
30
+ rspec-support (~> 3.8.0)
31
+ rspec-mocks (3.8.0)
32
+ diff-lcs (>= 1.2.0, < 2.0)
33
+ rspec-support (~> 3.8.0)
34
+ rspec-support (3.8.0)
35
+ rubocop (0.59.2)
36
+ jaro_winkler (~> 1.5.1)
37
+ parallel (~> 1.10)
38
+ parser (>= 2.5, != 2.5.1.1)
39
+ powerpack (~> 0.1)
40
+ rainbow (>= 2.2.2, < 4.0)
41
+ ruby-progressbar (~> 1.7)
42
+ unicode-display_width (~> 1.0, >= 1.0.1)
43
+ ruby-progressbar (1.10.0)
44
+ unicode-display_width (1.4.0)
45
+
46
+ PLATFORMS
47
+ ruby
48
+
49
+ DEPENDENCIES
50
+ cinnamon_serial!
51
+ pry
52
+ rspec
53
+ rubocop (~> 0.59.2)
54
+
55
+ BUNDLED WITH
56
+ 1.16.3
data/LICENSE ADDED
@@ -0,0 +1,7 @@
1
+ Copyright 2018 Blue Marble Payroll, LLC
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
4
+
5
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6
+
7
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,212 @@
1
+ # Cinnamon Serial
2
+
3
+ [![Build Status](https://travis-ci.org/bluemarblepayroll/cinnamon_serial.svg?branch=master)](https://travis-ci.org/bluemarblepayroll/cinnamon_serial)
4
+
5
+ A common issue is that we typically want different data going
6
+ outbound than what we have available server-side. Some example motivations could be:
7
+
8
+ * I have too much data, I want to slim down the outbound request.
9
+ * I do not have enough data, I need to get more and send that as well.
10
+ * I do not want to expose some data due to security/authorization concerns.
11
+
12
+ Having a separate layer that specializes in this type of materialization is important no matter
13
+ what the reasons are. This library provides a simple domain-specific language that makes creating
14
+ serializers or serialization layers declarative and easy.
15
+
16
+ ## Installation
17
+
18
+ To install through Rubygems:
19
+
20
+ ````
21
+ gem install install cinnamon_serial
22
+ ````
23
+
24
+ You can also add this to your Gemfile:
25
+
26
+ ````
27
+ bundle add cinnamon_serial
28
+ ````
29
+
30
+ ## Examples
31
+
32
+ ### Getting Started
33
+
34
+ Consider the following class:
35
+
36
+ ```
37
+ class Employee
38
+ attr_accessor :id, :first_name, :last_name, :active, :account, :progress, :start_date
39
+ end
40
+ ```
41
+
42
+ We could create a simple 1:1 serializer like so:
43
+
44
+ ```
45
+ class EmployeeSerializer < CinnamonSerial::Base
46
+ serialize :id, :first_name, :last_name, :active, :account, :progress, :start_date
47
+ end
48
+ ```
49
+
50
+ To use this serializer:
51
+
52
+ ```
53
+ employee = Employee.new
54
+ # populate employee data...
55
+ serializer = EmployeeSerializer.new(employee)
56
+ data = serializer.as_json
57
+ ```
58
+
59
+ The 'data' variable above is now a hash with only data as specified by the serializer.
60
+
61
+ ### Dynamic Attributes
62
+
63
+ Serialized keys do not have to match the composed object. Using our examples above we could
64
+ create another serializer:
65
+
66
+ ```
67
+ class EmployeeListSerializer < CinnamonSerial::Base
68
+ serialize :id
69
+ serialize :proper_name, to: :last_name
70
+ end
71
+ ```
72
+
73
+ In this case the serialized data will contain a key 'proper_name' instead of 'last_name'.
74
+
75
+ ### Calling Methods
76
+
77
+ serialized keys are not limited to just attributes, in fact, it will just test the model to see if the
78
+ composed object responds to the key and if it does it will send to the object. For example:
79
+
80
+ ```
81
+ class FormalEmployee < Employee
82
+ def proper_name
83
+ "#{last_name}, #{first_name}"
84
+ end
85
+ end
86
+ ```
87
+
88
+ ```
89
+ class EmployeeListSerializer < CinnamonSerial::Base
90
+ serialize :id, :proper_name
91
+ end
92
+ ```
93
+
94
+ ### Value Aliasing
95
+
96
+ Note: Internationalization is an incredibly complex problem that this library will not try to solve but it does provide the ability to override resolved values. In the future the aliasing and formatting abilities should be extracted and plugged-in as to provide internationalization support.
97
+
98
+ You are allowed to override the value if a value has been resolved to one of the following:
99
+
100
+ 1. true
101
+ 2. false
102
+ 3. nil
103
+ 4. 'present'
104
+ 5. 'blank'
105
+
106
+ Say you want to show Yes/No/Unknown for a boolean value. Building on our previous Employee examples we could modify our EmployeeSerializer:
107
+
108
+ ```
109
+ class EmployeeSerializer < CinnamonSerial::Base
110
+ serialize :id, :first_name, :last_name
111
+ serialize :active, true_alias: 'Yes', false_alias: 'No', null: 'Unknown'
112
+ end
113
+ ```
114
+
115
+ Now the value of active will be 'Yes', 'No', or 'Unknown' instead of true, false, or null.
116
+
117
+ ### Value Formatting
118
+
119
+ Two basic formatters that come included with this library are:
120
+
121
+ 1. Masking (defaults to masking all but last 4 characters with character X)
122
+ 2. Percent Formatting (two decimal places)
123
+
124
+ An example of custom formatters would be:
125
+
126
+ ```
127
+ class EmployeeSerializer < CinnamonSerial::Base
128
+ serialize :id, :first_name, :last_name
129
+ serialize :account, mask: true
130
+ serialize :progress, percent: true
131
+ end
132
+ ```
133
+
134
+ Account will be formatted as a masked string and progress will be converted to a percent formatted string.
135
+
136
+ ### Custom Methods
137
+
138
+ There are two ways to specify to execute a method on the serializer:
139
+
140
+ 1. Method - call an instance method with no arguments.
141
+ 2. Transform - Resolve the value first then pass it into an instance method.
142
+
143
+ For example:
144
+
145
+ ```
146
+ class EmployeeSerializer < CinnamonSerial::Base
147
+ serialize :id, :first_name, :last_name, :start_date
148
+ serialize :renewal_date, for: :start_date, transform: true
149
+ serialize :user_id, method: true
150
+
151
+ private
152
+
153
+ def renewal_date(date)
154
+ # Two years out
155
+ date + (60 * 60 * 24 * 24)
156
+ end
157
+
158
+ def user_id
159
+ dig_opt(:user, :id)
160
+ end
161
+ end
162
+ ```
163
+
164
+ Some notes about the above example:
165
+
166
+ * You can either pass in true (it will call the method named as the key) or the explicit name of the method.
167
+ * dig_opt is a convenience method that will call dig on the serializer options (second argument in serializer constructor.)
168
+
169
+ ### Custom Code
170
+
171
+ In you need full control over serialization you can create hydrate blocks of code that will execute (in order of declaration.) For example:
172
+
173
+ ```
174
+ class EmployeeSerializer < CinnamonSerial::Base
175
+ serialize :id, :first_name, :last_name, :start_date
176
+ serialize :active, manual: true
177
+
178
+ hydrate do
179
+ set_active(obj.start_date >= Date.today)
180
+ end
181
+ end
182
+ ```
183
+
184
+ some notes about the above example:
185
+
186
+ * setting manual to true declares that no mapping should be automatically performed. Instead, you must set it within the hydrate block.
187
+ * set_* are magic methods that will set the value to the value passed in. In this context we will set the 'active' value to true if the start_date is either today or earlier than today.
188
+ * obj is the composed object (in this context it would be the Employee instance.) You are allowed to access it using the obj getter. In the same vain, you are also allowed to access the serializer options using 'opts' getter.
189
+
190
+ ## Contributing
191
+
192
+ ### Development Environment Configuration
193
+
194
+ Basic steps to take to get this repository compiling:
195
+
196
+ 1. Install [Ruby](https://www.ruby-lang.org/en/documentation/installation/) (check cinnamon_serial.gemspec for versions supported)
197
+ 2. Install bundler (gem install bundler)
198
+ 3. Clone the repository (git clone git@github.com:bluemarblepayroll/cinnamon_serial.git)
199
+ 4. Navigate to the root folder (cd cinnamon_serial)
200
+ 5. Install dependencies (bundle)
201
+
202
+ ### Running Tests
203
+
204
+ To execute the test suite run:
205
+
206
+ ````
207
+ rspec
208
+ ````
209
+
210
+ ## License
211
+
212
+ This project is MIT Licensed.
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require './lib/cinnamon_serial/version'
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = 'cinnamon_serial'
7
+ s.version = CinnamonSerial::VERSION
8
+ s.summary = 'Domain-specific language for serialization specification.'
9
+
10
+ s.description = <<-DESCRIPTION
11
+ Domain-specific language for serialization specification.
12
+ DESCRIPTION
13
+
14
+ s.authors = ['Matthew Ruggio']
15
+ s.email = ['mruggio@bluemarblepayroll.com']
16
+ s.files = `git ls-files`.split("\n")
17
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
18
+ s.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
19
+ s.homepage = 'https://github.com/bluemarblepayroll/cinnamon_serial'
20
+ s.license = 'MIT'
21
+
22
+ s.required_ruby_version = '>= 2.3.1'
23
+
24
+ s.add_development_dependency('pry')
25
+ s.add_development_dependency('rspec')
26
+ s.add_development_dependency('rubocop', '~> 0.59.2')
27
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require_relative 'cinnamon_serial/cinnamon_serial'
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module CinnamonSerial
11
+ # This is the main parent class that all serializers must inherit from.
12
+ class Base
13
+ extend Dsl
14
+
15
+ class << self
16
+ def map(enumerable, opts = {})
17
+ enumerable.map { |e| new(e, opts) }
18
+ end
19
+ end
20
+
21
+ attr_reader :data,
22
+ :obj,
23
+ :opts,
24
+ :klasses
25
+
26
+ def initialize(obj, opts = {}, klasses = Set.new)
27
+ @obj = obj
28
+ @opts = opts || {}
29
+ @klasses = klasses
30
+
31
+ materialize_data
32
+ execute_hydrate_blocks
33
+ end
34
+
35
+ def dig_opt(*keys)
36
+ opts.dig(*keys)
37
+ end
38
+
39
+ def as_json(_options = {})
40
+ data
41
+ end
42
+
43
+ def respond_to_missing?(method_sym)
44
+ data.key?(method_sym.to_s) || super
45
+ end
46
+
47
+ def method_missing(method_sym, *arguments, &block)
48
+ key = method_sym.to_s.sub('set_', '')
49
+
50
+ if data.key?(method_sym.to_s)
51
+ data[method_sym.to_s]
52
+ elsif data.key?(key)
53
+ @data[key] = arguments[0]
54
+ else
55
+ super
56
+ end
57
+ end
58
+
59
+ def [](attr)
60
+ send(attr)
61
+ end
62
+
63
+ private
64
+
65
+ def inherited_cinnamon_serial_specification
66
+ self.class.inherited_cinnamon_serial_specification
67
+ end
68
+
69
+ def materialize_data
70
+ @data = {}
71
+
72
+ inherited_cinnamon_serial_specification.attribute_map.each do |key, options|
73
+ @data[key.to_s] = options.resolve(self, key)
74
+ end
75
+
76
+ nil
77
+ end
78
+
79
+ def execute_hydrate_blocks
80
+ inherited_cinnamon_serial_specification.hydrate_blocks.each do |block|
81
+ if block && block.arity == 1
82
+ block.call(self)
83
+ elsif block
84
+ instance_eval(&block)
85
+ end
86
+ end
87
+
88
+ nil
89
+ end
90
+ end
91
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'set'
11
+
12
+ require_relative 'formatting'
13
+ require_relative 'resolver'
14
+ require_relative 'specification'
15
+ require_relative 'dsl'
16
+ require_relative 'base'
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module CinnamonSerial
11
+ # This module includes all the class-level methods used to specify serializers.
12
+ module Dsl
13
+ def cinnamon_serial_specification
14
+ @cinnamon_serial_specification ||= Specification.new
15
+ end
16
+
17
+ def serialize(*keys)
18
+ cinnamon_serial_specification.set(keys)
19
+
20
+ nil
21
+ end
22
+ # <b>DEPRECATED:</b> Please use <tt>serialize</tt> instead.
23
+ alias present serialize
24
+
25
+ def hydrate(&block)
26
+ cinnamon_serial_specification.hydrate(block)
27
+
28
+ nil
29
+ end
30
+
31
+ def inherited_cinnamon_serial_specification
32
+ return @inherited_cinnamon_serial_specification if @inherited_cinnamon_serial_specification
33
+
34
+ attribute_map = {}
35
+ hydrate_blocks = []
36
+
37
+ # We need to reverse this so parents go first.
38
+ ancestors.reverse_each do |ancestor|
39
+ next unless ancestor.respond_to?(:cinnamon_serial_specification)
40
+
41
+ specification = ancestor.cinnamon_serial_specification
42
+
43
+ attribute_map.merge!(specification.attribute_map)
44
+ hydrate_blocks += specification.hydrate_blocks
45
+ end
46
+
47
+ @inherited_cinnamon_serial_specification = Specification.new(
48
+ attribute_map: attribute_map,
49
+ hydrate_blocks: hydrate_blocks
50
+ )
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module CinnamonSerial
11
+ # Static utility methods for general use.
12
+ class Formatting
13
+ class << self
14
+ # Only show the last N positions in a string, replace the
15
+ # rest with the mask_with value.
16
+ # Example:
17
+ # - 123-45-6789 becomes: XXXXXXX6789
18
+ # - ABCDEFG becomes: XXXDEFG
19
+ def mask(value, keep_last = 4, mask_with = 'X')
20
+ string_value = value.to_s
21
+ return string_value if blank?(string_value) || string_value.size <= keep_last
22
+
23
+ (mask_with.to_s * (string_value.size - keep_last)) + string_value[-keep_last..-1]
24
+ end
25
+
26
+ def percent(num)
27
+ present?(num) ? format('%.2f %', num) : ''
28
+ end
29
+
30
+ def present?(value)
31
+ !blank?(value)
32
+ end
33
+
34
+ def blank?(value)
35
+ if value.respond_to?(:blank?)
36
+ value.blank?
37
+ elsif value.respond_to?(:empty?)
38
+ !!value.empty?
39
+ else
40
+ !value
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module CinnamonSerial
11
+ # Class that allows an engineer to specify what to do about mapping a key for a serializer.
12
+ class Resolver
13
+ attr_accessor :as,
14
+ :blank,
15
+ # <b>DEPRECATED:</b> Please use <tt>false_alias</tt> instead.
16
+ :false,
17
+ :false_alias,
18
+ :for,
19
+ :manual,
20
+ :mask,
21
+ :mask_char,
22
+ :mask_len,
23
+ :method,
24
+ :null,
25
+ :percent,
26
+ :present,
27
+ :through,
28
+ :transform,
29
+ # <b>DEPRECATED:</b> Please use <tt>true_alias</tt> instead.
30
+ :true,
31
+ :true_alias
32
+
33
+ def initialize(options = {})
34
+ @option_keys = options.keys.map(&:to_s).to_set
35
+
36
+ options.each do |key, value|
37
+ raise ArgumentError, "Illegal option: #{key}" unless respond_to?(key)
38
+
39
+ send("#{key}=", value)
40
+ end
41
+ end
42
+
43
+ def resolve(presenter, key)
44
+ raise ArgumentError, 'Presenter is required' unless presenter
45
+
46
+ return if manual
47
+
48
+ # Get the value
49
+ value = resolve_value(presenter, key)
50
+
51
+ # Transform the value
52
+ value = resolve_transform(presenter, key, value)
53
+ value = resolve_alias(value)
54
+ value = resolve_as(presenter, value)
55
+
56
+ # Format the value
57
+ value = resolve_percent(value)
58
+ resolve_mask(value)
59
+ end
60
+
61
+ private
62
+
63
+ # (method) and (for/through) are mutually exlusive use-cases.
64
+ # Example: you would never use for and method.
65
+ def resolve_value(presenter, key)
66
+ # If you pass in something that is not true boolean value then use that as a method name
67
+ # to call on the presenter.
68
+ return presenter.send(key) if method.is_a?(TrueClass)
69
+ return presenter.send(method) if method.to_s.length.positive?
70
+
71
+ # User for/through
72
+ model_key = self.for || key
73
+ model = presenter.obj
74
+
75
+ Array(through).each do |association|
76
+ model = model.respond_to?(association) ? model.send(association) : nil
77
+
78
+ break unless model
79
+ end
80
+
81
+ model&.respond_to?(model_key) ? model.send(model_key) : nil
82
+ end
83
+
84
+ def resolve_transform(presenter, key, value)
85
+ return presenter.send(key, value) if transform.is_a?(TrueClass)
86
+ return presenter.send(transform, value) if Formatting.present?(transform)
87
+
88
+ value
89
+ end
90
+
91
+ def resolve_alias(value)
92
+ if @option_keys.include?('true_alias') && value.is_a?(TrueClass)
93
+ true_alias
94
+ # <b>DEPRECATED:</b> Please use <tt>true_alias</tt> instead.
95
+ elsif @option_keys.include?('true') && value.is_a?(TrueClass)
96
+ self.true
97
+ elsif @option_keys.include?('false_alias') && value.is_a?(FalseClass)
98
+ false_alias
99
+ # <b>DEPRECATED:</b> Please use <tt>false_alias</tt> instead.
100
+ elsif @option_keys.include?('false') && value.is_a?(FalseClass)
101
+ self.false
102
+ elsif @option_keys.include?('null') && value.nil?
103
+ null
104
+ elsif @option_keys.include?('blank') && Formatting.blank?(value)
105
+ blank
106
+ elsif @option_keys.include?('present') && Formatting.present?(value)
107
+ present
108
+ else
109
+ value
110
+ end
111
+ end
112
+
113
+ def resolve_mask(value)
114
+ mask ? Formatting.mask(value, mask_len || 4, mask_char || 'X') : value
115
+ end
116
+
117
+ def resolve_percent(value)
118
+ percent ? Formatting.percent(value) : value
119
+ end
120
+
121
+ def resolve_as(presenter, value)
122
+ return value unless as
123
+ return nil unless value
124
+
125
+ class_constant = as_class_constant
126
+
127
+ # If we already serialized this type, lets not do it again.
128
+ # This will prevent endless cycles / loops.
129
+ return nil if presenter.klasses.include?(class_constant.to_s)
130
+
131
+ # We do not want to create a hard dependency on ActiveRecord/Rails in this gem,
132
+ # but we can still create a soft dependency in case it was included as a peer.
133
+ value = value.to_a if value.class.name == 'ActiveRecord::Relation'
134
+
135
+ new_klasses = presenter.klasses + Set[class_constant.to_s]
136
+
137
+ if value.is_a?(Array)
138
+ value.map { |v| class_constant.new(v, presenter.opts, new_klasses) }
139
+ else
140
+ class_constant.new(value, presenter.opts, new_klasses)
141
+ end
142
+ end
143
+
144
+ def as_class_name
145
+ return nil unless as
146
+
147
+ non_constant_types = %w[String Symbol]
148
+
149
+ # If we have a peer dependency for ActiveSupport then lets use it.
150
+ if non_constant_types.include?(as.class.name) && as.to_s.respond_to?(:classify)
151
+ as.to_s.classify
152
+ elsif non_constant_types.include?(as.class.name)
153
+ as.to_s
154
+ else
155
+ as
156
+ end
157
+ end
158
+
159
+ def as_class_constant
160
+ return nil unless as
161
+
162
+ class_name = as_class_name
163
+
164
+ # If we have a peer dependency for ActiveSupport then lets use it.
165
+ if class_name.is_a?(String) && class_name.respond_to?(:constantize)
166
+ class_name.constantize
167
+ elsif class_name.is_a?(String)
168
+ Object.const_get(class_name)
169
+ else
170
+ class_name
171
+ end
172
+ end
173
+ end
174
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module CinnamonSerial
11
+ # A Specification is a group of attribute mappings and custom code blocks to execute
12
+ # for a serializer.
13
+ class Specification
14
+ attr_reader :attribute_map, :hydrate_blocks
15
+
16
+ def initialize(attribute_map: {}, hydrate_blocks: [])
17
+ @attribute_map = attribute_map
18
+ @hydrate_blocks = hydrate_blocks
19
+ end
20
+
21
+ def set(*keys)
22
+ keys = keys.flatten
23
+
24
+ # We have been sent options
25
+ options = Resolver.new(keys.last.is_a?(Hash) ? keys.pop : {})
26
+
27
+ raise ArgumentError, 'keys cannot be empty' if keys.empty?
28
+
29
+ keys.each { |key| @attribute_map[key.to_s] = options }
30
+
31
+ nil
32
+ end
33
+
34
+ def hydrate(block)
35
+ @hydrate_blocks << block
36
+
37
+ nil
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ module CinnamonSerial
11
+ VERSION = '1.0.0'
12
+ end
data/spec/base_spec.rb ADDED
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'date'
11
+ require 'pry'
12
+ require './lib/cinnamon_serial'
13
+ require './spec/examples'
14
+
15
+ describe CinnamonSerial::Base do
16
+ let(:employee_list_keys) do
17
+ %w[
18
+ active
19
+ id
20
+ name
21
+ user_id
22
+ user_name
23
+ other_id
24
+ manager_name
25
+ renewal_date
26
+ notify_date
27
+ true_value
28
+ true_alias_value
29
+ false_value
30
+ false_alias_value
31
+ null
32
+ present
33
+ blank
34
+ manager
35
+ employees
36
+ account
37
+ progress
38
+ ]
39
+ end
40
+
41
+ let(:employee_keys) do
42
+ employee_list_keys + %w[
43
+ start_date
44
+ job
45
+ founder
46
+ owner
47
+ ]
48
+ end
49
+
50
+ let(:nick) do
51
+ Employee.new(
52
+ id: 2,
53
+ name: 'nick',
54
+ start_date: Date.new(1350, 12, 12),
55
+ job: 'electrician',
56
+ account: '1234567890',
57
+ progress: 55.5,
58
+ employees: [
59
+ Employee.new(
60
+ id: 1,
61
+ name: 'matt',
62
+ start_date:
63
+ Date.new(1750, 12, 12),
64
+ job: 'plumber',
65
+ account: '98765443322111',
66
+ progress: 10.98
67
+ )
68
+ ]
69
+ )
70
+ end
71
+
72
+ let(:matt) do
73
+ Employee.new(
74
+ id: 1,
75
+ name: 'matt',
76
+ start_date:
77
+ Date.new(1750, 12, 12),
78
+ job: 'plumber',
79
+ account: '98765443322111',
80
+ progress: 10.98,
81
+ manager: nick
82
+ )
83
+ end
84
+
85
+ let(:opts) { { user: { id: 100, name: 'Frank Rizzo' }, active: true } }
86
+ let(:employee_serializer) { EmployeeSerializer.new(matt, opts) }
87
+ let(:data) { employee_serializer.data }
88
+
89
+ it 'should materialize_data and execute hydrate blocks for superclass' do
90
+ expect(data['id']).to eq(matt.id)
91
+ expect(data['name']).to eq(matt.name)
92
+ expect(data['user_id']).to eq(opts[:user][:id])
93
+ expect(data['user_name']).to eq(opts[:user][:name])
94
+ expect(data['other_id']).to eq(matt.id)
95
+ expect(data['manager_name']).to eq(nick.name)
96
+ expect(data['renewal_date']).to eq(matt.start_date + (60 * 60 * 24 * 24))
97
+ expect(data['notify_date']).to eq(matt.start_date + (60 * 60 * 24 * 23))
98
+
99
+ expect(data['true_value']).to eq('I am true.')
100
+ expect(data['true_alias_value']).to eq('I am true alias.')
101
+ expect(data['false_value']).to eq('I am false.')
102
+ expect(data['false_alias_value']).to eq('I am false alias.')
103
+ expect(data['null']).to eq('I am null.')
104
+ expect(data['present']).to eq('I am present.')
105
+ expect(data['blank']).to eq('I am blank.')
106
+ expect(data['manager']).to be_a_kind_of(EmployeeSerializer)
107
+
108
+ expect(data['account']).to eq('XXXXXXXXXX2111')
109
+ expect(data['progress']).to eq('10.98 %')
110
+ end
111
+
112
+ it 'should materialize_data and execute hydrate blocks for subclass' do
113
+ expect(data['active']).to eq(opts[:active])
114
+ expect(data['start_date']).to eq(matt.start_date)
115
+ expect(data['job']).to eq(matt.job)
116
+ expect(data['founder']).to eq(matt.id < 10)
117
+ expect(data['owner']).to eq(matt.id == 1)
118
+ end
119
+
120
+ it 'should not create cycles when flattening out presenters' do
121
+ expect(data['manager'].employees).to be nil
122
+ end
123
+ end
data/spec/dsl_spec.rb ADDED
@@ -0,0 +1,81 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ require 'date'
11
+ require 'pry'
12
+ require './lib/cinnamon_serial'
13
+ require './spec/examples'
14
+
15
+ describe CinnamonSerial::Dsl do
16
+ let(:employee_list_keys) do
17
+ %w[
18
+ active
19
+ id
20
+ name
21
+ user_id
22
+ user_name
23
+ other_id
24
+ manager_name
25
+ renewal_date
26
+ notify_date
27
+ true_value
28
+ true_alias_value
29
+ false_value
30
+ false_alias_value
31
+ null
32
+ present
33
+ blank
34
+ manager
35
+ employees
36
+ account
37
+ progress
38
+ ]
39
+ end
40
+
41
+ let(:employee_keys) do
42
+ employee_list_keys + %w[
43
+ start_date
44
+ job
45
+ founder
46
+ owner
47
+ ]
48
+ end
49
+
50
+ it 'should include all attribute mappings' do
51
+ specification = EmployeeListSerializer.cinnamon_serial_specification
52
+ attribute_map = specification.attribute_map
53
+ keys = attribute_map.keys
54
+
55
+ expect(specification).to be_a_kind_of(CinnamonSerial::Specification)
56
+ expect(keys.count).to eq(employee_list_keys.count)
57
+ expect(keys).to eq(employee_list_keys)
58
+ end
59
+
60
+ context 'with inheritance' do
61
+ it 'should include only its immediate attribute mappings' do
62
+ specification = EmployeeSerializer.cinnamon_serial_specification
63
+ attribute_map = specification.attribute_map
64
+ keys = attribute_map.keys
65
+
66
+ expect(specification).to be_a_kind_of(CinnamonSerial::Specification)
67
+ expect(keys.count).to eq(4)
68
+ expect(keys).to eq(%w[start_date job founder owner])
69
+ end
70
+
71
+ it 'should include its immediate and ancestor attribute mappings' do
72
+ specification = EmployeeSerializer.inherited_cinnamon_serial_specification
73
+ attribute_map = specification.attribute_map
74
+ keys = attribute_map.keys
75
+
76
+ expect(specification).to be_a_kind_of(CinnamonSerial::Specification)
77
+ expect(keys.count).to eq(employee_keys.count)
78
+ expect(keys).to eq(employee_keys)
79
+ end
80
+ end
81
+ end
data/spec/examples.rb ADDED
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ #
4
+ # Copyright (c) 2018-present, Blue Marble Payroll, LLC
5
+ #
6
+ # This source code is licensed under the MIT license found in the
7
+ # LICENSE file in the root directory of this source tree.
8
+ #
9
+
10
+ class Employee
11
+ attr_accessor :id,
12
+ :name,
13
+ :start_date,
14
+ :job,
15
+ :account,
16
+ :progress,
17
+ :manager,
18
+ :employees
19
+
20
+ def initialize(
21
+ id:,
22
+ name:,
23
+ start_date:,
24
+ job:,
25
+ account:,
26
+ progress:,
27
+ manager: nil,
28
+ employees: []
29
+ )
30
+ @id = id
31
+ @name = name
32
+ @start_date = start_date
33
+ @job = job
34
+ @account = account
35
+ @progress = progress
36
+ @manager = manager
37
+ @employees = employees
38
+ end
39
+
40
+ def true_alias_value
41
+ true
42
+ end
43
+ alias true_value true_alias_value
44
+
45
+ def false_alias_value
46
+ false
47
+ end
48
+ alias false_value false_alias_value
49
+
50
+ def null
51
+ nil
52
+ end
53
+
54
+ def blank
55
+ ''
56
+ end
57
+
58
+ def present
59
+ 'abc123.'
60
+ end
61
+ end
62
+
63
+ # ######################################################
64
+ # A Class that exemplifies all possible mapping options.
65
+ # ######################################################
66
+ class EmployeeListSerializer < CinnamonSerial::Base
67
+ # Test skipping mapping using 'manual'
68
+ serialize :active, manual: true
69
+
70
+ # ################
71
+ # Value Resolution
72
+ # ################
73
+
74
+ # Test basic 1:1 mapping
75
+ serialize :id, :name
76
+
77
+ # Test presenter 'method'
78
+ serialize :user_id, method: true
79
+ serialize :user_name, method: :formatted_user_name
80
+
81
+ # Test 'for'
82
+ serialize :other_id, for: :id
83
+
84
+ # Test 'for' and 'through'
85
+ serialize :manager_name, for: :name, through: :manager
86
+
87
+ # ################
88
+ # Value Aliasing
89
+ # ################
90
+
91
+ # Test presenter 'transform' method
92
+ serialize :renewal_date, for: :start_date, transform: true
93
+ serialize :notify_date, for: :start_date, transform: :notification_date
94
+
95
+ # Test 'true' alias
96
+
97
+ # <b>DEPRECATED:</b> Please use <tt>true_alias</tt> instead.
98
+ serialize :true_value, true: 'I am true.'
99
+ serialize :true_alias_value, true_alias: 'I am true alias.'
100
+
101
+ # Test 'false' alias
102
+
103
+ # <b>DEPRECATED:</b> Please use <tt>false_alias</tt> instead.
104
+ serialize :false_value, false: 'I am false.'
105
+ serialize :false_alias_value, false_alias: 'I am false alias.'
106
+
107
+ # Test 'null' alias
108
+ serialize :null, null: 'I am null.'
109
+
110
+ # Test 'present' alias
111
+ serialize :present, present: 'I am present.'
112
+
113
+ # Test 'blank' alias
114
+ serialize :blank, blank: 'I am blank.'
115
+
116
+ # Test 'as' conversion
117
+ serialize :manager, as: 'EmployeeSerializer'
118
+ serialize :employees, as: 'EmployeeSerializer'
119
+
120
+ # ################
121
+ # Value Formatting
122
+ # ################
123
+
124
+ serialize :account, mask: true
125
+ serialize :progress, percent: true
126
+
127
+ # #######################
128
+ # Manual Hydration Blocks
129
+ # #######################
130
+ hydrate do
131
+ set_active(dig_opt(:active))
132
+ end
133
+
134
+ # ##############
135
+ # Custom Methods
136
+ # ##############
137
+
138
+ private
139
+
140
+ def user_id
141
+ dig_opt(:user, :id)
142
+ end
143
+
144
+ def formatted_user_name
145
+ dig_opt(:user, :name)
146
+ end
147
+
148
+ def renewal_date(date)
149
+ # Two years out
150
+ date + (60 * 60 * 24 * 24)
151
+ end
152
+
153
+ def notification_date(date)
154
+ # 23 months out
155
+ date + (60 * 60 * 24 * 23)
156
+ end
157
+ end
158
+
159
+ # #############################
160
+ # Subclass to test inheritance.
161
+ # #############################
162
+ class EmployeeSerializer < EmployeeListSerializer
163
+ present :start_date, :job
164
+
165
+ present :founder,
166
+ :owner, manual: true
167
+
168
+ hydrate do
169
+ set_founder(obj.id < 10)
170
+ end
171
+
172
+ hydrate do
173
+ set_owner(obj.id == 1)
174
+ end
175
+ end
metadata ADDED
@@ -0,0 +1,111 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cinnamon_serial
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Matthew Ruggio
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-10-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: pry
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rspec
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rubocop
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: 0.59.2
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: 0.59.2
55
+ description: " Domain-specific language for serialization specification.\n"
56
+ email:
57
+ - mruggio@bluemarblepayroll.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".editorconfig"
63
+ - ".gitignore"
64
+ - ".rubocop.yml"
65
+ - ".rubocop_todo.yml"
66
+ - ".ruby-version"
67
+ - ".travis.yml"
68
+ - Gemfile
69
+ - Gemfile.lock
70
+ - LICENSE
71
+ - README.md
72
+ - cinnamon_serial.gemspec
73
+ - lib/cinnamon_serial.rb
74
+ - lib/cinnamon_serial/base.rb
75
+ - lib/cinnamon_serial/cinnamon_serial.rb
76
+ - lib/cinnamon_serial/dsl.rb
77
+ - lib/cinnamon_serial/formatting.rb
78
+ - lib/cinnamon_serial/resolver.rb
79
+ - lib/cinnamon_serial/specification.rb
80
+ - lib/cinnamon_serial/version.rb
81
+ - spec/base_spec.rb
82
+ - spec/dsl_spec.rb
83
+ - spec/examples.rb
84
+ homepage: https://github.com/bluemarblepayroll/cinnamon_serial
85
+ licenses:
86
+ - MIT
87
+ metadata: {}
88
+ post_install_message:
89
+ rdoc_options: []
90
+ require_paths:
91
+ - lib
92
+ required_ruby_version: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: 2.3.1
97
+ required_rubygems_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ requirements: []
103
+ rubyforge_project:
104
+ rubygems_version: 2.5.2.3
105
+ signing_key:
106
+ specification_version: 4
107
+ summary: Domain-specific language for serialization specification.
108
+ test_files:
109
+ - spec/base_spec.rb
110
+ - spec/dsl_spec.rb
111
+ - spec/examples.rb