normalizy 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +3 -0
  3. data/LICENSE +21 -0
  4. data/README.md +341 -0
  5. data/lib/generators/normalizy/install_generator.rb +13 -0
  6. data/lib/generators/normalizy/templates/config/initializers/normalizy.rb +7 -0
  7. data/lib/normalizy/config.rb +35 -0
  8. data/lib/normalizy/extensions.rb +105 -0
  9. data/lib/normalizy/filters/number.rb +17 -0
  10. data/lib/normalizy/filters/strip.rb +19 -0
  11. data/lib/normalizy/filters.rb +8 -0
  12. data/lib/normalizy/rspec/matcher.rb +115 -0
  13. data/lib/normalizy/version.rb +5 -0
  14. data/lib/normalizy.rb +20 -0
  15. data/spec/normalizy/config/add_spec.rb +17 -0
  16. data/spec/normalizy/config/alias_spec.rb +63 -0
  17. data/spec/normalizy/config/default_filters_spec.rb +19 -0
  18. data/spec/normalizy/config/initialize_spec.rb +12 -0
  19. data/spec/normalizy/config/normalizy_aliases_spec.rb +9 -0
  20. data/spec/normalizy/config/normalizy_raws_spec.rb +9 -0
  21. data/spec/normalizy/extensions/apply_normalizations_spec.rb +163 -0
  22. data/spec/normalizy/extensions/normalizy_rules_spec.rb +244 -0
  23. data/spec/normalizy/extensions/normalizy_spec.rb +11 -0
  24. data/spec/normalizy/filters/number_spec.rb +15 -0
  25. data/spec/normalizy/filters/strip_spec.rb +13 -0
  26. data/spec/normalizy/normalizy/configure_spec.rb +11 -0
  27. data/spec/normalizy/rspec/matcher/description_spec.rb +26 -0
  28. data/spec/normalizy/rspec/matcher/failure_message_spec.rb +70 -0
  29. data/spec/normalizy/rspec/matcher/failure_message_when_negated_spec.rb +32 -0
  30. data/spec/normalizy/rspec/matcher/from_spec.rb +18 -0
  31. data/spec/normalizy/rspec/matcher/matchers_spec.rb +97 -0
  32. data/spec/normalizy/rspec/matcher/to_spec.rb +18 -0
  33. data/spec/normalizy/rspec/normalizy_spec.rb +8 -0
  34. data/spec/rails_helper.rb +9 -0
  35. data/spec/support/common.rb +11 -0
  36. data/spec/support/db/schema.rb +13 -0
  37. data/spec/support/filters/blacklist_filter.rb +15 -0
  38. data/spec/support/models/clean.rb +4 -0
  39. data/spec/support/models/user.rb +9 -0
  40. metadata +210 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 781b68ea361e5e2a698bba7230a2a2459f2cb758
4
+ data.tar.gz: 01d9c17b15ee6ec08c5dda0da645897a43f9370d
5
+ SHA512:
6
+ metadata.gz: cef08c0e44de84158e91659510c0d4bcd50715739a977452c9c7037b44dbaa06acfe29993388b357a1c0e863e7cafa22e8b5acbc8f995b740340c8efa7aeef76
7
+ data.tar.gz: 632e6116a884839be1e018f905599766ecf9223ef9336ae5ba79c503bd549c41242fdcab975a489372f75a530f97b495ec71e0f53d649e0c8cf844c870788341
data/CHANGELOG.md ADDED
@@ -0,0 +1,3 @@
1
+ ## v0.1.0
2
+
3
+ - First release.
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2017 Washington Botelho
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,341 @@
1
+ # Normalizy
2
+
3
+ [![Build Status](https://travis-ci.org/wbotelhos/normalizy.svg)](https://travis-ci.org/wbotelhos/normalizy)
4
+ [![Gem Version](https://badge.fury.io/rb/normalizy.svg)](https://badge.fury.io/rb/normalizy)
5
+
6
+ Attribute normalizer for ActiveRecord.
7
+
8
+ ## Description
9
+
10
+ If you know the obvious format of an input, why not normalize it instead of raise an validation error to your use? Make the follow email ` myemail@example.org ` valid like `email@example.com` with no need to override acessors methods.
11
+
12
+ ## install
13
+
14
+ Add the following code on your `Gemfile` and run `bundle install`:
15
+
16
+ ```ruby
17
+ gem 'normalizy'
18
+ ```
19
+
20
+ So generates an initializer for future custom configurations:
21
+
22
+ ```ruby
23
+ rails g normalizy:install
24
+ ```
25
+
26
+ It will generates a file `config/initializers/normalizy.rb` where you can configure you own normalizer and choose some defaults one.
27
+
28
+ ## Usage
29
+
30
+ On your model, just add `normalizy` callback with the attribute you want to normalize and the filter to be used:
31
+
32
+ ```ruby
33
+ class User < ApplicationRecord
34
+ normalizy :name, with: :strip
35
+ end
36
+ ```
37
+
38
+ Now some email like ` myemail@example.org ` will be saved as `email@example.com`.
39
+
40
+ ## Filters
41
+
42
+ We have a couple of built-in filters. The most one is just a wrapper o the original String methods:
43
+
44
+ ### Number
45
+
46
+ ```ruby
47
+ normalizy :age, with: :number
48
+
49
+ ' 32'
50
+ # 32
51
+ ```
52
+
53
+ By default, `number` works with input value before [Type Cast](#type-cast)
54
+ ### Strip
55
+
56
+ Options:
57
+
58
+ - `side`: `:left`, `:right` or `:both`. Default: `:both`
59
+
60
+ ```ruby
61
+ normalizy :name, with: :strip
62
+ ' Washington Botelho '
63
+ # 'Washington Botelho'
64
+ ```
65
+
66
+ ```ruby
67
+ normalizy :name, with: { strip: { side: :left } }
68
+ ' Washington Botelho '
69
+ # 'Washington Botelho '
70
+ ```
71
+
72
+ ```ruby
73
+ normalizy :name, with: { strip: { side: :right } }
74
+ ' Washington Botelho '
75
+ # ' Washington Botelho'
76
+ ```
77
+
78
+ ```ruby
79
+ normalizy :name, with: { strip: { side: :both } }
80
+ ' Washington Botelho '
81
+ # 'Washington Botelho'
82
+ ```
83
+
84
+ As you can see, the rules can be passed as Symbol/String or as Hash if it has options.
85
+
86
+ ## Multiple Filters
87
+
88
+ You can normalize with a couple of filters at once:
89
+
90
+ ```ruby
91
+ normalizy :name, with: %i[squish titleize]
92
+ ' washington botelho '
93
+ # 'Washington Botelho'
94
+ ```
95
+
96
+ ## Multiple Attributes
97
+
98
+ You can normalize more than one attribute at once too, with one or muiltiple filters:
99
+
100
+ ```ruby
101
+ normalizy :email, :username, with: :downcase
102
+ ```
103
+
104
+ Of course you can declare muiltiples attribute and multiple filters together.
105
+ It is possible to make sequential normalizy calls:
106
+
107
+ ```ruby
108
+ normalizy :email, :username, with: :squish
109
+ normalizy :email , with: :downcase
110
+ ```
111
+
112
+ In this case, each line will be evaluated from the top to the bottom.
113
+
114
+ ## Default Filters
115
+
116
+ You can configure some default filters to be runned. Edit you initializer at `config/initializers/normalizy.rb`:
117
+
118
+ ```ruby
119
+ Normalizy.configure do |config|
120
+ config.default_filters = [:squish]
121
+ end
122
+ ```
123
+
124
+ Now, all normalization will include squish, even when no rule is declared.
125
+
126
+ ```ruby
127
+ normalizy :name
128
+ ' Washington Botelho '
129
+ # 'Washington Botelho'
130
+ ```
131
+
132
+ If you declare some filter, the default filter `squish` will be runned together:
133
+
134
+ ```ruby
135
+ normalizy :name, with: :downcase
136
+ ' washington botelho '
137
+ # 'Washington Botelho'
138
+ ```
139
+
140
+ ## Custom Filter
141
+
142
+ You can create a custom filter that implements `call` method with an `input` as argument and an optional `options`:
143
+
144
+ ```ruby
145
+ module Normalizy
146
+ module Filters
147
+ module Blacklist
148
+ def self.call(input)
149
+ input.gsub 'Fuck', replacement: '***'
150
+ end
151
+ end
152
+ end
153
+ end
154
+ ```
155
+
156
+ ```ruby
157
+ Normalizy.configure do |config|
158
+ config.add :blacklist, Normalizy::Filters::Blacklist
159
+ end
160
+ ```
161
+
162
+ Now you can use your custom filter:
163
+
164
+ ```ruby
165
+ normalizy :name, with: :blacklist
166
+
167
+ 'Washington Fuck Botelho'
168
+ # 'Washington *** Botelho'
169
+ ```
170
+
171
+ If you want to pass options to your filter, just call it as hash and the value will be passed to the custom filter:
172
+
173
+ ```ruby
174
+ module Normalizy
175
+ module Filters
176
+ module Blacklist
177
+ def self.call(input, options: {})
178
+ input.gsub 'Fuck', replacement: options[:replacement]
179
+ end
180
+ end
181
+ end
182
+ end
183
+ ```
184
+
185
+ ```ruby
186
+ normalizy :name, with: blacklist: { replacement: '---' }
187
+
188
+ 'Washington Fuck Botelho'
189
+ # 'Washington --- Botelho'
190
+ ```
191
+
192
+ You can pass a block and it will be received on filter:
193
+
194
+ ```ruby
195
+ module Normalizy
196
+ module Filters
197
+ module Blacklist
198
+ def self.call(input, options: {})
199
+ value = input.gsub('Fuck', 'filtered')
200
+
201
+ value = yield(value) if block_given?
202
+
203
+ value
204
+ end
205
+ end
206
+ end
207
+ end
208
+ ```
209
+
210
+ ```ruby
211
+ normalizy :name, with: :blacklist, &->(value) { value.sub('filtered', '(filtered 2x)') }
212
+
213
+ 'Washington Fuck Botelho'
214
+ # 'Washington (filtered 2x) Botelho'
215
+ ```
216
+
217
+ The block
218
+
219
+ ## Method Filters
220
+
221
+ If a built-in filter is not found, Normalizy will try to find a method to suply the normalize with the same name of the given filter:
222
+
223
+ ```ruby
224
+ normalizy :birthday, with: :parse_date
225
+
226
+ def parse_date(input, options = {})
227
+ Time.zone.parse(input).strftime '%Y/%m/%d'
228
+ end
229
+
230
+ '1984-10-23'
231
+ # '1984/10/23'
232
+ ```
233
+
234
+ If you gives an option, it will be passed to the function too:
235
+
236
+ ```ruby
237
+ normalizy :birthday, with: { parse_date: { format: '%Y/%m/%d' }
238
+
239
+ def parse_date(input, options = {})
240
+ Time.zone.parse(input).strftime options[:format]
241
+ end
242
+
243
+ '1984-10-23'
244
+ # '1984/10/23'
245
+ ```
246
+
247
+ Block methods works here too.
248
+
249
+ ## Native Filter
250
+
251
+ After the missing built-in and class method, the fallback will be the value of native methods.
252
+
253
+ ```ruby
254
+ normalizy :name, with: :reverse
255
+
256
+ 'Washington Botelho'
257
+ # "ohletoB notgnihsaW"
258
+ ```
259
+
260
+ ## Inline Filter
261
+
262
+ Maybe you want to declare an inline filter, in this case, just use a Lambda or Proc:
263
+
264
+ ```ruby
265
+ normalizy :age, with: ->(input) { input.abs }
266
+
267
+ -32
268
+ # 32
269
+ ```
270
+
271
+ You can use it on filters declaration too:
272
+
273
+ ```ruby
274
+ Normalizy.configure do |config|
275
+ config.add :age, ->(input) { input.abs }
276
+ end
277
+ ```
278
+
279
+ ## Type Cast
280
+
281
+ An input field with `$ 42.00` dollars when sent to model with a field with `integer` type,
282
+ will be converted to `0`, since the type does not match. But you want to use the value before Rails do this cast the type.
283
+
284
+ To receive the value before type cast, just pass a `raw` options as `true`:
285
+
286
+ ```ruby
287
+ normalizy :amount, with: :number, raw: true
288
+
289
+ '$ 42.00'
290
+ # 4200
291
+ ```
292
+
293
+ To avoid repeat the `raw: true` where you will always to use, you can register a filter with this options:
294
+
295
+ ```ruby
296
+ Normalizy.configure do |config|
297
+ config.add :money, ->(input) { input.gsub(/\D/, '') }, raw: true
298
+ end
299
+ ```
300
+
301
+ ## Alias
302
+
303
+ Sometimes you want to give a better name to your filter, just to keep the things semantic.
304
+ But duplicates the code just to redefine a new name is not a good idea, so, just create an alias:
305
+
306
+ ```ruby
307
+ Normalizy.configure do |config|
308
+ config.alias :money, :number
309
+ end
310
+ ```
311
+
312
+ Now, `money` will delegate to `number` filter.
313
+ Since we already know the need of `raw` options, we can declare it here too:
314
+
315
+ ```ruby
316
+ Normalizy.configure do |config|
317
+ config.alias :money, :number, raw: true
318
+ end
319
+ ```
320
+
321
+ But `number` filter already works with `raw: true`, don't need to tell it again.
322
+ An our previous example, about `amount`, was refactored to:
323
+
324
+ ```ruby
325
+ normalizy :amount, with: :money
326
+
327
+ '$ 42.00'
328
+ # 4200
329
+ ```
330
+
331
+ If you need to alias multiple filters, just provide an array of them:
332
+
333
+ ```ruby
334
+ Normalizy.configure do |config|
335
+ config.alias :username, %i[squish downcase]
336
+ end
337
+ ```
338
+
339
+ ## Love it!
340
+
341
+ Via [PayPal](https://www.paypal.com/cgi-bin/webscr?cmd=_donations&business=X8HEP2878NDEG&item_name=normalizy) or [Gratipay](https://gratipay.com/~wbotelhos). Thanks! (:
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Normalizy
4
+ class InstallGenerator < Rails::Generators::Base
5
+ source_root File.expand_path('../templates', __FILE__)
6
+
7
+ desc 'creates an initializer'
8
+
9
+ def copy_initializer
10
+ copy_file 'config/initializers/normalizy.rb', 'config/initializers/normalizy.rb'
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ Normalizy.configure do |config|
4
+ config.default_filters = [:squish]
5
+
6
+ # config.filters[:blacklist] = Blacklist
7
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'normalizy/filters'
4
+
5
+ module Normalizy
6
+ class Config
7
+ attr_accessor :default_filters
8
+ attr_reader :filters, :normalizy_aliases, :normalizy_raws
9
+
10
+ def add(name, value, raw: false)
11
+ @filters[name] = value
12
+ @normalizy_raws << name if raw
13
+
14
+ self
15
+ end
16
+
17
+ def alias(name, to, raw: false)
18
+ @normalizy_aliases[name] = to
19
+ @normalizy_raws << name if raw
20
+
21
+ self
22
+ end
23
+
24
+ def initialize
25
+ @default_filters = {}
26
+ @normalizy_aliases = {}
27
+ @normalizy_raws = [:number]
28
+
29
+ @filters = {
30
+ number: Normalizy::Filters::Number,
31
+ strip: Normalizy::Filters::Strip
32
+ }
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Normalizy
4
+ module Extension
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_validation :apply_normalizy, if: -> {
9
+ self.class.respond_to? :normalizy
10
+ }
11
+
12
+ private
13
+
14
+ def apply_normalizy
15
+ (self.class.normalizy_rules || {}).each do |attribute, rules|
16
+ rules.each { |rule| normalizy! rule.merge(attribute: attribute) }
17
+ end
18
+ end
19
+
20
+ def extract_filter(rule, filters: Normalizy.config.filters)
21
+ if rule.is_a?(Hash)
22
+ result = filters[rule.keys.first] || rule.keys.first
23
+ options = rule.values.first
24
+ else
25
+ result = filters[rule]
26
+ end
27
+
28
+ [result || rule, options || {}]
29
+ end
30
+
31
+ def extract_value(value, filter, options, block)
32
+ if filter.respond_to?(:call)
33
+ if filter.method(:call).arity == -2
34
+ filter.call value, options, &block
35
+ else
36
+ filter.call value, &block
37
+ end
38
+ elsif respond_to?(filter)
39
+ send filter, value, options, &block
40
+ elsif value.respond_to?(filter)
41
+ value.send filter, &block
42
+ else
43
+ value
44
+ end
45
+ end
46
+
47
+ def normalizy!(attribute:, rules:, options:, block:)
48
+ return if rules.blank? && block.blank?
49
+
50
+ aliases = Normalizy.config.normalizy_aliases
51
+ value = nil
52
+
53
+ [rules].flatten.compact.each do |rule|
54
+ value = original_value(attribute, rule, options)
55
+ aliased_rules = [aliases.key?(rule) ? aliases[rule] : rule]
56
+
57
+ aliased_rules.flatten.compact.each do |alias_rule|
58
+ filter, filter_options = extract_filter(alias_rule)
59
+ value = extract_value(value, filter, filter_options, block)
60
+ end
61
+ end
62
+
63
+ return unless value
64
+
65
+ write attribute, value
66
+ end
67
+
68
+ def original_value(attribute, rule, options)
69
+ if raw? attribute, rule, options
70
+ send "#{attribute}_before_type_cast"
71
+ else
72
+ send attribute
73
+ end
74
+ end
75
+
76
+ def raw?(attribute, rule, options)
77
+ return false unless respond_to?("#{attribute}_before_type_cast")
78
+
79
+ options[:raw] || Normalizy.config.normalizy_raws.include?(rule)
80
+ end
81
+
82
+ def write(attribute, value)
83
+ write_attribute attribute, value
84
+ rescue ActiveModel::MissingAttributeError
85
+ send "#{attribute}=", value
86
+ end
87
+ end
88
+
89
+ module ClassMethods
90
+ attr_accessor :normalizy_rules
91
+
92
+ def normalizy(*args, &block)
93
+ options = args.extract_options!
94
+ rules = options[:with] || Normalizy.config.default_filters
95
+
96
+ self.normalizy_rules ||= {}
97
+
98
+ args.each do |field|
99
+ normalizy_rules[field] ||= []
100
+ normalizy_rules[field] << { block: block, options: options.except(:with), rules: rules }
101
+ end
102
+ end
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Normalizy
4
+ module Filters
5
+ module Number
6
+ def self.call(input)
7
+ return input unless input.is_a?(String)
8
+
9
+ value = input.gsub(/\D/, '')
10
+
11
+ return nil if value.blank?
12
+
13
+ value.to_i
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Normalizy
4
+ module Filters
5
+ module Strip
6
+ def self.call(input, options = {})
7
+ return input unless input.is_a?(String)
8
+
9
+ regex = {
10
+ both: '\A\s*|\s*\z',
11
+ left: '\A\s*',
12
+ right: '\s*\z'
13
+ }[options[:side] || :both]
14
+
15
+ input.gsub Regexp.new(/#{regex}/), ''
16
+ end
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Normalizy
4
+ module Filters
5
+ end
6
+ end
7
+
8
+ Dir["#{File.dirname(__FILE__)}/filters/*.rb"].each { |file| require file }
@@ -0,0 +1,115 @@
1
+ module Normalizy
2
+ module RSpec
3
+ def normalizy(attribute)
4
+ Matcher.new attribute
5
+ end
6
+
7
+ class Matcher
8
+ def initialize(attribute)
9
+ @attribute = attribute
10
+ end
11
+
12
+ def description
13
+ return "normalizy #{@attribute} with #{with_expected}" if @with.present?
14
+
15
+ "normalizy #{@attribute} from #{from_value} to #{to_value}"
16
+ end
17
+
18
+ def failure_message
19
+ return "expected: #{with_expected}\n got: #{actual_value}" if @with.present?
20
+
21
+ "expected: #{to_value}\n got: #{actual_value}"
22
+ end
23
+
24
+ def failure_message_when_negated
25
+ return "expected: value != #{with_expected}\n got: #{actual_value}" if @with.present?
26
+
27
+ "expected: value != #{to_value}\n got: #{actual_value}"
28
+ end
29
+
30
+ def from(value)
31
+ @from = value
32
+
33
+ self
34
+ end
35
+
36
+ def matches?(subject)
37
+ @subject = subject
38
+
39
+ if @with.present?
40
+ options = @subject.class.normalizy_rules[@attribute]
41
+
42
+ return false if options.blank?
43
+
44
+ options.each do |option|
45
+ rules = option[:rules]
46
+
47
+ return true if rules.is_a?(Array) && rules.include?(@with)
48
+ return true if rules == @with
49
+ end
50
+
51
+ false
52
+ else
53
+ @subject.send "#{@attribute}=", @from
54
+
55
+ @subject.send(@attribute) == @to
56
+ end
57
+ end
58
+
59
+ def to(value)
60
+ @to = value
61
+
62
+ self
63
+ end
64
+
65
+ def with(value)
66
+ @with = value
67
+
68
+ self
69
+ end
70
+
71
+ private
72
+
73
+ def actual_value
74
+ return with_value if @with
75
+
76
+ value = @subject.send(@attribute)
77
+
78
+ value.is_a?(String) ? %("#{value}") : value
79
+ end
80
+
81
+ def from_value
82
+ @from.nil? ? :nil : %("#{@from}")
83
+ end
84
+
85
+ def to_value
86
+ @to.nil? ? :nil : %("#{@to}")
87
+ end
88
+
89
+ def with_expected
90
+ @with
91
+ end
92
+
93
+ def with_value
94
+ options = @subject.class.normalizy_rules[@attribute]
95
+
96
+ return :nil if options.nil?
97
+ return %("#{options}") if options.blank?
98
+
99
+ result = options.map do |option|
100
+ rules = option[:rules]
101
+
102
+ if rules.nil?
103
+ :nil
104
+ elsif rules.blank?
105
+ %("#{rules}")
106
+ else
107
+ rules
108
+ end
109
+ end
110
+
111
+ result.join ', '
112
+ end
113
+ end
114
+ end
115
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Normalizy
4
+ VERSION = '0.1.0'
5
+ end