rordash 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,95 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ module FileUtil
5
+ FILE_EXTENSION_MATCH = /^.*\.\S+\w$/.freeze
6
+ CONTENT_TYPE_TO_EXT_MATCH = %r{/(.*)$}.freeze
7
+ UNSUPPORTED_CONTENT_TYPE_MAP = {
8
+ 'application/vnd.ms-publisher' => 'pub'
9
+ }.freeze
10
+
11
+ class << self
12
+ def filename_with_ext_from(filename:, content_type:)
13
+ filename = filename.to_s.strip
14
+ return filename if filename.blank?
15
+ return filename if filename_has_extension?(filename)
16
+ return filename if content_type.blank?
17
+
18
+ ext = content_type_to_extension(content_type)
19
+ return filename if ext.blank?
20
+
21
+ "#{filename}.#{ext}"
22
+ end
23
+
24
+ def content_type_to_extension(content_type)
25
+ return content_type if content_type.blank?
26
+
27
+ ext = mime_type_for_content_type(content_type)&.preferred_extension
28
+ ext = UNSUPPORTED_CONTENT_TYPE_MAP[content_type] if ext.blank?
29
+ ext = content_type.match(CONTENT_TYPE_TO_EXT_MATCH).to_a.last if ext.blank?
30
+ ext
31
+ end
32
+
33
+ def fixture_file_path(filename)
34
+ rel_path = PathUtil.fixtures_path('files')
35
+ path = rel_path.join(filename)
36
+
37
+ if path.exist?
38
+ path
39
+ else
40
+ msg = "the directory '%s' does not contain a file named '%s'"
41
+ raise ArgumentError, format(msg, rel_path, filename)
42
+ end
43
+ end
44
+
45
+ def fixture_file_path_str(filename)
46
+ fixture_file_path(filename).to_s
47
+ end
48
+
49
+ def read_fixture_file(rel_path)
50
+ File.read(PathUtil.fixtures_path(rel_path))
51
+ end
52
+
53
+ def open_fixture_file(filename)
54
+ pathname = Utils::FileUtil.fixture_file_path(filename)
55
+ return nil unless pathname.exist?
56
+
57
+ ::File.open(pathname.to_s)
58
+ end
59
+
60
+ def read_fixture_file_as_hash(rel_path)
61
+ HashUtil.from_string(read_fixture_file(rel_path))
62
+ end
63
+
64
+ def create_file_blob(filename:, content_type: nil, metadata: nil)
65
+ content_type = content_type.present? ? content_type : content_type_from_filename(filename) || Mime[:jpg]
66
+
67
+ raise StandardError, "ActiveStorage must be installed" unless defined? ActiveStorage::Blob
68
+
69
+ ActiveStorage::Blob.create_after_upload! io: ::File.open(fixture_file_path(filename).to_s), filename: filename,
70
+ content_type: content_type, metadata: metadata
71
+ end
72
+
73
+ def file_url_for(filename)
74
+ Addressable::URI.parse(Faker::Internet.url)&.join(filename).to_s
75
+ end
76
+
77
+ def content_type_from_filename(filename)
78
+ mime_type = mime_type_from_filename(filename)
79
+ mime_type&.content_type
80
+ end
81
+
82
+ def mime_type_from_filename(filename)
83
+ MIME::Types.type_for(filename).first
84
+ end
85
+
86
+ def mime_type_for_content_type(content_type)
87
+ MIME::Types[content_type].first
88
+ end
89
+
90
+ def filename_has_extension?(filename)
91
+ filename.to_s.match?(FILE_EXTENSION_MATCH)
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ # rubocop:disable Metrics/ModuleLength
5
+ module HashUtil
6
+ ROOT_PATHS = %w[. *].freeze
7
+
8
+ class << self
9
+ def from_string(json_str)
10
+ return json_str unless json_str.is_a?(String)
11
+
12
+ Oj.load(json_str, symbol_keys: true)
13
+ end
14
+
15
+ def to_str(obj)
16
+ return Oj.dump(obj.deep_stringify_keys) if hash_or_array?(obj)
17
+
18
+ obj.to_s
19
+ end
20
+
21
+ def pretty(obj)
22
+ return obj unless hash_or_array?(obj)
23
+
24
+ JSON.pretty_generate(obj)
25
+ end
26
+
27
+ def get(obj, path, default: nil)
28
+ return obj if ROOT_PATHS.include?(path)
29
+
30
+ value = R_.get(obj, path.to_s)
31
+ return default if value.nil?
32
+
33
+ value
34
+ end
35
+
36
+ def get_first_present(obj, dotted_paths)
37
+ dotted_paths.each do |path|
38
+ value = R_.get(obj, path)
39
+ return value if value.present?
40
+ end
41
+
42
+ nil
43
+ end
44
+
45
+ def set(hash, path, value)
46
+ R_.set(hash, path.to_s, value)
47
+ end
48
+
49
+ def group_by(obj, key_or_proc)
50
+ proc = if key_or_proc.is_a?(Proc)
51
+ key_or_proc
52
+ else
53
+ ->(hash) { R_.get(hash, key_or_proc.to_s) }
54
+ end
55
+
56
+ R_.group_by(obj, proc)
57
+ end
58
+
59
+ def dot(hash, keep_arrays: true, &block)
60
+ return Dottie.flatten(hash) unless keep_arrays
61
+
62
+ results = {}
63
+ Dottie.flatten(hash, intermediate: true).each do |k, v|
64
+ next if RegexUtil.match?(:dotted_index, k)
65
+ next if v.is_a?(::Hash)
66
+
67
+ value = block ? yield(k, v) : v
68
+ results.merge!(k => value)
69
+ end
70
+
71
+ results
72
+ end
73
+
74
+ def deep_key?(obj, key)
75
+ return false unless obj.is_a?(::Hash)
76
+ return obj.key?(key) unless key.is_a?(String) && key.include?('.')
77
+
78
+ dotted_keys(obj).include?(key)
79
+ end
80
+
81
+ def dotted_keys(obj, keep_arrays: true)
82
+ dot(obj, keep_arrays: keep_arrays).keys
83
+ end
84
+
85
+ # rubocop:disable Metrics/MethodLength,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/AbcSize
86
+ def pick(hash, paths, keep_arrays: true)
87
+ all_paths = paths.is_a?(Array) ? paths.map(&:to_s) : [paths.to_s]
88
+ has_deep_paths = all_paths.any? { |path| path.include?('.') }
89
+
90
+ results = {}
91
+ dotted_hash = has_deep_paths ? Dottie.flatten(hash, intermediate: true) : hash
92
+ filtered_keys = dotted_hash.keys.select { |path| all_paths.include?(path.to_s) }
93
+ filtered_dotted_hash = dotted_hash.slice(*filtered_keys)
94
+
95
+ return filtered_dotted_hash unless has_deep_paths
96
+
97
+ if keep_arrays
98
+ filtered_dotted_hash.each_pair { |k, v| Utils::HashUtil.set(results, k, v) }
99
+ return results
100
+ else
101
+ filtered_dotted_hash.each_pair do |dotted_key, val|
102
+ stringify_dotted_key = dotted_key.to_s
103
+ next if all_paths.exclude?(stringify_dotted_key)
104
+
105
+ should_reconstruct_array = RegexUtil.match?(:dotted_index, stringify_dotted_key)
106
+ key = should_reconstruct_array ? dotted_key.split('[').first : dotted_key
107
+ value = should_reconstruct_array ? (results[key] || []).push(val) : val
108
+
109
+ results.merge!(key => value)
110
+ end
111
+ end
112
+
113
+ undot(results)
114
+ end
115
+ # rubocop:enable Metrics/MethodLength,Metrics/PerceivedComplexity,Metrics/CyclomaticComplexity,Metrics/AbcSize
116
+
117
+ def undot(hash, &block)
118
+ results = {}
119
+ Dottie.flatten(hash, { intermediate: false }).each do |k, v|
120
+ value = block ? yield(k, v) : v
121
+ set(results, k, value)
122
+ end
123
+ results
124
+ end
125
+
126
+ def deep_compact(attrs, each_value_proc: nil)
127
+ result = {}
128
+
129
+ dot(attrs, keep_arrays: true) do |k, v|
130
+ value = each_value_proc.respond_to?(:call) ? each_value_proc&.call(k, v) : v.compact
131
+ next if value.nil?
132
+
133
+ set(result, k, value)
134
+ end
135
+
136
+ result
137
+ end
138
+
139
+ def reject_blank_values(obj)
140
+ return obj unless hash_or_array?(obj)
141
+
142
+ obj.compact.reduce({}) do |memo, (k, v)|
143
+ v = v.is_a?(String) ? v.strip : v
144
+ next memo if !hash_or_array?(v) && v.blank?
145
+
146
+ memo.merge(k => v)
147
+ end
148
+ end
149
+
150
+ def deep_reject_blank_values(attrs)
151
+ deep_compact(attrs, each_value_proc: lambda do |_k, v|
152
+ v = v.is_a?(String) ? v.strip : v
153
+ v = v.compact if hash_or_array?(v)
154
+ if v.is_a?(Array)
155
+ v = v.reject do |val|
156
+ val = val.is_a?(String) ? val.strip : val
157
+ val.blank?
158
+ end
159
+ end
160
+ v.blank? ? nil : v
161
+ end)
162
+ end
163
+
164
+ def deep_symbolize_keys(obj)
165
+ return obj unless hash_or_array?(obj)
166
+ return obj.deep_symbolize_keys if obj.is_a?(::Hash)
167
+
168
+ obj.map { |item| item.is_a?(::Hash) ? item.deep_symbolize_keys : item }
169
+ end
170
+
171
+ def digest(obj)
172
+ Digest::SHA1.base64digest Marshal.dump(obj)
173
+ end
174
+
175
+ def hash_or_array?(obj)
176
+ obj.is_a?(::Hash) || obj.is_a?(Array)
177
+ end
178
+ end
179
+ end
180
+ # rubocop:enable Metrics/ModuleLength
181
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ module NumericUtil
5
+ class << self
6
+ module Unit
7
+ FEET = :ft
8
+ METER = :m
9
+ end
10
+
11
+ def numeric?(value)
12
+ !Float(value.to_s).nil?
13
+ rescue StandardError
14
+ false
15
+ end
16
+
17
+ def convert_unit(value, from_unit:, to_unit:)
18
+ Measured::Length.new(value, from_unit).convert_to(to_unit).value.to_f
19
+ end
20
+
21
+ def convert_unit_sq(value, from_unit:, to_unit:)
22
+ value = value.is_a?(String) ? BigDecimal(value) : value
23
+ val = convert_unit(value, from_unit: from_unit, to_unit: to_unit)
24
+ (val * val) / value
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ module ObjectUtil
5
+ class << self
6
+ def to_class(classname)
7
+ classname.constantize if classname.is_a?(String)
8
+ classname
9
+ end
10
+
11
+ def to_classname(klass)
12
+ return klass.name if Object.const_defined?(klass)
13
+
14
+ nil
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ module PathUtil
5
+ class << self
6
+ def fixtures_path(path = '')
7
+ Rails.root.join('spec', 'fixtures', path)
8
+ end
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ module RegexUtil
5
+ EMAIL_MATCH = URI::MailTo::EMAIL_REGEXP
6
+ POSTAL_CODE_MATCH = /[A-Za-z]\d[A-Za-z][ -]?\d[A-Za-z]\d/.freeze
7
+ DOTTED_INDEX_MATCH = /\w+\[\d+\]/.freeze
8
+ URI_MATCH = URI::DEFAULT_PARSER.make_regexp.freeze
9
+
10
+ TYPE = {
11
+ email: EMAIL_MATCH,
12
+ uri: URI_MATCH,
13
+ postal_code: POSTAL_CODE_MATCH,
14
+ dotted_index: DOTTED_INDEX_MATCH
15
+ }.freeze
16
+
17
+ class << self
18
+ def match?(type, value)
19
+ TYPE[type].match?(value)
20
+ end
21
+
22
+ def match(type, value)
23
+ TYPE[type].match(value)
24
+ end
25
+
26
+ def replace(type, value, replacement)
27
+ return value unless value.is_a?(String)
28
+
29
+ regex = TYPE[type]
30
+ return value if regex.nil?
31
+
32
+ value.sub(regex, replacement)
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ module UrlUtil
5
+ class << self
6
+ def safe_escape(url)
7
+ Addressable::URI.escape(url).to_s
8
+ end
9
+
10
+ def error_from_http_status(http_status)
11
+ Rack::Utils::HTTP_STATUS_CODES[http_status.to_i] || 'Invalid HTTP Status Code'
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ VERSION = "0.1.0"
5
+ end
data/lib/rordash.rb ADDED
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ %w[
4
+ version
5
+ debug_util
6
+ regex_util
7
+ hash_util
8
+ path_util
9
+ file_util
10
+ url_util
11
+ object_util
12
+ numeric_util
13
+ ].each do |filename|
14
+ require File.expand_path("../rordash/#{filename}", Pathname.new(__FILE__).realpath)
15
+ end
16
+
17
+ module Rordash
18
+ ; end
data/rordash.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require File.expand_path('../lib/rordash/version', __FILE__)
4
+
5
+ Gem::Specification.new do |gem|
6
+ gem.authors = ["Desmond O'Leary"]
7
+ gem.email = ["desoleary@gmail.com"]
8
+ gem.description = %q{Lodash inspired utilities}
9
+ gem.summary = %q{Lodash inspired utilities}
10
+ gem.homepage = "https://github.com/omnitech-solutions/rordash"
11
+ gem.license = "MIT"
12
+
13
+ gem.files = `git ls-files`.split($\)
14
+ gem.executables = gem.files.grep(%r{^exe/}).map{ |f| File.basename(f) }
15
+ gem.test_files = gem.files.grep(%r{^(spec|features)/})
16
+ gem.name = "rordash"
17
+ gem.require_paths = ["lib"]
18
+ gem.version = Rordash::VERSION
19
+ gem.required_ruby_version = ">= 2.6.0"
20
+
21
+ gem.metadata["homepage_uri"] = gem.homepage
22
+ gem.metadata["source_code_uri"] = gem.homepage
23
+ gem.metadata["changelog_uri"] = "#{gem.homepage}/CHANGELOG.md"
24
+
25
+ gem.add_runtime_dependency 'stackprof', '>= 0.2'
26
+ gem.add_runtime_dependency 'colorize', '~> 0.8.1'
27
+ gem.add_runtime_dependency 'mime-types', '>= 3'
28
+ gem.add_runtime_dependency 'activesupport', '>= 5'
29
+ gem.add_runtime_dependency 'activestorage', '>= 5'
30
+ gem.add_runtime_dependency 'rack', '>= 2'
31
+ gem.add_runtime_dependency 'faker', '>= 2'
32
+ gem.add_runtime_dependency 'oj', '>= 3'
33
+ gem.add_runtime_dependency 'rudash', '>= 4'
34
+ gem.add_runtime_dependency 'dottie', '>= 0.0.2'
35
+ gem.add_runtime_dependency 'addressable', '>= 2.6.0'
36
+ gem.add_runtime_dependency 'measured', '>= 2.5'
37
+
38
+ gem.add_development_dependency("rake", "~> 13.0.6")
39
+ gem.add_development_dependency("rspec", "~> 3.12.0")
40
+ gem.add_development_dependency("simplecov", "~> 0.21.2")
41
+ gem.add_development_dependency("codecov", "~> 0.6.0")
42
+ end
data/sig/rordash.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module Rordash
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ if ENV['RUN_COVERAGE_REPORT']
4
+ require 'simplecov'
5
+
6
+ SimpleCov.start do
7
+ add_filter 'vendor/'
8
+ add_filter %r{^/spec/}
9
+ end
10
+ SimpleCov.minimum_coverage_by_file 90
11
+
12
+ require 'codecov'
13
+ SimpleCov.formatter = SimpleCov::Formatter::Codecov
14
+ end
@@ -0,0 +1,3 @@
1
+ name,date,favorite_color
2
+ John Smith,Oct 2 1901,blue
3
+ Gemma Jones,Sept 1 2018,silver
@@ -0,0 +1,6 @@
1
+ [
2
+ {
3
+ "color": "red",
4
+ "value": "#f00"
5
+ }
6
+ ]
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ # rubocop:disable RSpec/MessageSpies
5
+ RSpec.describe DebugUtil do
6
+ describe '.calculate_duration' do
7
+ let(:tag) { nil }
8
+
9
+ subject(:calculated_duration) { described_class.calculate_duration(tag: tag) { 'do-something' } }
10
+
11
+ context 'without block' do
12
+ it 'raises missing block error' do
13
+ expect { described_class.calculate_duration }.to raise_error ArgumentError, 'Missing block'
14
+ end
15
+ end
16
+
17
+ context 'with tag' do
18
+ let(:tag) { 'some-tag' }
19
+
20
+ it 'includes tag' do
21
+ expect { calculated_duration }.to output(Regexp.new(/tag: `#{tag}` - total duration - /)).to_stdout
22
+ end
23
+ end
24
+
25
+ it 'prints duration' do
26
+ expect { calculated_duration }.to output(/tag: `default` - total duration - /).to_stdout
27
+ end
28
+ end
29
+
30
+ describe '.wrap_stack_prof' do
31
+ let(:tag) { nil }
32
+ let(:out) { nil }
33
+
34
+ subject(:profile_wrapper) { described_class.wrap_stack_prof(tag: tag, out: out) { 'do something' } }
35
+
36
+ it 'wraps block inside stackprof runner' do
37
+ expect(StackProf).to receive(:run).with(mode: :wall, out: 'tmp/stackprof.dump', raw: true, interval: 1000)
38
+
39
+ profile_wrapper
40
+ end
41
+
42
+ it 'prints output file location' do
43
+ expect(StackProf).to receive(:run).with(mode: :wall, out: 'tmp/stackprof.dump', raw: true, interval: 1000)
44
+
45
+ expect { profile_wrapper }.to output(Regexp.new(%r{StackProf output file: tmp/stackprof.dump})).to_stdout
46
+ end
47
+ end
48
+ end
49
+ # rubocop:enable RSpec/MessageSpies
50
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rordash
4
+ RSpec.describe FileUtil do
5
+ let(:filename) { 'sample.csv' }
6
+ let(:rel_path) { "files/#{filename}" }
7
+
8
+ describe '.fixture_file_path' do
9
+ it 'returns file contents' do
10
+ actual = described_class.fixture_file_path(filename)
11
+
12
+ expect(actual.exist?).to be_truthy
13
+ expect(actual.to_s).to include("spec/fixtures/files/#{filename}")
14
+ end
15
+ end
16
+
17
+ describe '#fixture_file_path_str' do
18
+ it 'returns file contents' do
19
+ expect(described_class.fixture_file_path_str(filename)).to include("spec/fixtures/files/#{filename}")
20
+ end
21
+ end
22
+
23
+ describe '#read_fixture_file' do
24
+ it 'returns file contents' do
25
+ expect(described_class.read_fixture_file(rel_path)).to include("Gemma Jones,Sept 1 2018,silver")
26
+ end
27
+ end
28
+
29
+ describe '#read_fixture_file_as_hash' do
30
+ let(:rel_path) { 'files/sample.json' }
31
+
32
+ it 'returns file contents' do
33
+ expect(described_class.read_fixture_file_as_hash(rel_path)).to eql([
34
+ {
35
+ color: "red",
36
+ value: "#f00"
37
+ }
38
+ ])
39
+ end
40
+ end
41
+
42
+ describe '.content_type_from_filename' do
43
+ let(:filename) { 'sample.pdf' }
44
+
45
+ subject(:content_type) { described_class.content_type_from_filename(filename) }
46
+
47
+ it 'returns content type' do
48
+ expect(content_type).to eql('application/pdf')
49
+ end
50
+ end
51
+
52
+ describe '.mime_type_from_filename' do
53
+ let(:filename) { 'sample.pdf' }
54
+
55
+ subject(:mime_type) { described_class.mime_type_from_filename(filename) }
56
+
57
+ it 'returns content type' do
58
+ expect(mime_type).to eql(MIME::Types['application/pdf'].first)
59
+ end
60
+ end
61
+
62
+ describe '.file_url_for' do
63
+ let(:filename) { 'sample.pdf' }
64
+
65
+ subject(:file_url) { described_class.file_url_for(filename) }
66
+
67
+ it 'returns url with filename' do
68
+ expect(file_url).to match(RegexUtil::TYPE[:uri])
69
+ expect(file_url).to include(filename)
70
+ end
71
+ end
72
+
73
+ context 'with content types' do
74
+ let(:content_types) do
75
+ [
76
+ { content_type: 'image/png', extension: 'png' },
77
+ { content_type: 'text/csv', extension: 'csv' },
78
+ { content_type: 'application/msword', extension: 'doc' },
79
+ { content_type: 'text/html', extension: 'html' },
80
+ { content_type: 'application/zip', extension: 'zip' },
81
+ { content_type: 'image/bmp', extension: 'bmp' },
82
+ { content_type: 'application/vnd.ms-publisher', extension: 'pub' },
83
+ { content_type: 'image/jp2', extension: 'jp2' },
84
+ { content_type: 'video/mp4', extension: 'mp4' },
85
+ { content_type: 'application/pdf', extension: 'pdf' },
86
+ { content_type: 'image/svg+xml', extension: 'svg' },
87
+ { content_type: 'application/octet-stream', extension: 'bin' },
88
+ { content_type: 'image/webp', extension: 'webp' },
89
+ { content_type: 'image/gif', extension: 'gif' },
90
+ { content_type: 'text/plain', extension: 'txt' },
91
+ { content_type: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',
92
+ extension: 'pptx' },
93
+ { content_type: 'image/heic', extension: 'heic' },
94
+ { content_type: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
95
+ extension: 'docx' },
96
+ { content_type: 'image/jpeg', extension: 'jpeg' },
97
+ { content_type: 'image/tiff', extension: 'tiff' }
98
+ ]
99
+ end
100
+
101
+ describe '.filename_with_ext_from' do
102
+ let(:filename_without_extension) { 'filename' }
103
+
104
+ it 'returns the correct extension' do
105
+ content_types.each do |content_type:, extension:|
106
+ actual = described_class.filename_with_ext_from(filename: filename_without_extension,
107
+ content_type: content_type)
108
+ expect(actual).to eql("#{filename_without_extension}.#{extension}")
109
+ end
110
+ end
111
+ end
112
+
113
+ describe '.content_type_to_extension' do
114
+ it 'returns the correct extension' do
115
+ content_types.each do |content_type:, extension:|
116
+ actual = described_class.content_type_to_extension(content_type)
117
+ expect(actual).to eql(extension)
118
+ end
119
+ end
120
+ end
121
+ end
122
+ end
123
+ end