light_mapper 0.0.1 → 1.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: ec180a4b4062fa63e032fef32a556894a365dcb4
4
- data.tar.gz: 40f8416865d57b19af6c16bf0b82fe4306261dc1
2
+ SHA256:
3
+ metadata.gz: 309f037da546bc788d238d3fd43c7f92f1d117a1b885a30b7eaf65dc2947a445
4
+ data.tar.gz: f7a0c2c83eb49ce56ccbf1da8b102bf4325f868d9c35dee3a2dc36377f6c96f2
5
5
  SHA512:
6
- metadata.gz: 35af7945db5ac71c6353fa873f46a86677a9bcea56e9d4645c57acfbb2a850dc48ff4ee1e847d46fad8ca4bfb7a856ce69cc330cc57a1bf204ebcf2aa7ba719a
7
- data.tar.gz: 9e2693e1fbcc48e7a6a95c10d609aa3536303dcbad048bd52221516bbd96f3f1691775b6d1cceb75f7987c23f42fa4461e3c0dcc2b6f27aa003fba18f8efd8d9
6
+ metadata.gz: 9c306788435e091828219174645e0034c4bdfa8a096643d506e34258831bbefbbccf5dca2b0595720207796c301b01dc988abbd312d04391a9c0a5fc2b2d5dc2
7
+ data.tar.gz: 0fbd144a196b3edc3110c81e945d1d7ed6880f7d5cd8b82d34d1754bbdb3a32d7069254b03d5f8d977118c182fe8446734f555da92a144f130029c55c391ecce
data/.rubocop.yml CHANGED
@@ -15,10 +15,6 @@ Style/Documentation:
15
15
  Style/PercentLiteralDelimiters:
16
16
  Enabled: false
17
17
 
18
- # Offense count: 2
19
- Style/RegexpLiteral:
20
- MaxSlashes: 0
21
-
22
18
  # Offense count: 1
23
19
  # Cop supports --auto-correct.
24
20
  # Configuration parameters: EnforcedStyle, SupportedStyles.
@@ -45,4 +41,4 @@ Metrics/LineLength:
45
41
  AllCops:
46
42
  Exclude:
47
43
  - '**/Guardfile'
48
- - '**/light_parmas.gemspec'
44
+ - '**/light_mapper.gemspec'
data/.travis.yml CHANGED
@@ -1,6 +1,4 @@
1
1
  language: ruby
2
2
  script: bundle exec rake
3
3
  rvm:
4
- - 2.0.0
5
- - 2.1.0
6
- - 2.1.4
4
+ - 3.1.2
data/Gemfile CHANGED
@@ -2,3 +2,5 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in light_mapper.gemspec
4
4
  gemspec
5
+
6
+ ruby '2.7.4'
data/README.md CHANGED
@@ -1,6 +1,10 @@
1
1
  # LightMapper
2
2
 
3
- TODO: Write a gem description
3
+ [![Gem Version](https://badge.fury.io/rb/light_mapper.svg)](http://badge.fury.io/rb/light_mapper)
4
+ [![Build Status](https://travis-ci.org/pniemczyk/light_mapper.svg?branch=0.2.0)](https://travis-ci.org/pniemczyk/light_mapper)
5
+ [![Dependency Status](https://gemnasium.com/pniemczyk/light_mapper.svg)](https://gemnasium.com/pniemczyk/light_mapper)
6
+
7
+ This is simple mapper for hash.
4
8
 
5
9
  ## Installation
6
10
 
@@ -20,7 +24,152 @@ Or install it yourself as:
20
24
 
21
25
  ## Usage
22
26
 
23
- TODO: Write usage instructions here
27
+ ### Basic usage
28
+
29
+ ```ruby
30
+ {
31
+ 'FirstName' => 'Pawel',
32
+ 'LastName' => 'Niemczyk'
33
+ }.extend(LightMapper).mapping(
34
+ 'FirstName' => :first_name,
35
+ 'LastName' => :last_name
36
+ )
37
+ ```
38
+
39
+ or
40
+
41
+ ```ruby
42
+ LightMapper.mapping(
43
+ { 'FirstName' => 'Pawel', 'LastName' => 'Niemczyk' },
44
+ { 'FirstName' => :first_name, 'LastName' => :last_name }
45
+ )
46
+ ```
47
+
48
+ result is obvious:
49
+
50
+ ```ruby
51
+ { first_name: 'Pawel', last_name: 'Niemczyk' }
52
+ ```
53
+
54
+ The most popular usage:
55
+
56
+ ```ruby
57
+ PersonMapper = {
58
+ 'FirstName' => :first_name,
59
+ 'LastName' => :last_name,
60
+ 'Age' => 'age'
61
+ }
62
+
63
+ data = {
64
+ 'FirstName' => 'Pawel',
65
+ 'LastName' => 'Niemczyk',
66
+ 'Age' => 5
67
+ }
68
+
69
+ data.extend(LightMapper).mapping(PersonMapper)
70
+ # {first_name: 'Pawel', last_name: 'Niemczyk', 'age': 5}
71
+ ```
72
+
73
+ ### When you require all keys
74
+
75
+ ```ruby
76
+ { 'FirstName' => 'Pawel' }.extend(LightMapper).mapping({'FirstName' => :first_name, 'LastName' => :last_name}, strict: true)
77
+ ```
78
+
79
+ it will raise LightMapper::KeyMissing: LastName key not found; Full path LastName
80
+
81
+ ### When you want to pass string or symbol keys
82
+
83
+ ```ruby
84
+ {
85
+ 'FirstName' => 'Pawel',
86
+ second_name: 'Niemczyk'
87
+ }.extend(LightMapper).mapping({
88
+ 'FirstName' => :first_name,
89
+ 'second_name' => :last_name
90
+ }, any_keys: true)
91
+ ```
92
+
93
+ result will be:
94
+
95
+ ```ruby
96
+ { first_name: 'Pawel', last_name: 'Niemczyk' }
97
+ ```
98
+
99
+ ### Support for nested hashes, arrays and objects (now we talking what it is capable of)
100
+
101
+ ```ruby
102
+ {
103
+ 'source' => { 'google' => { 'search_word' => 'ruby' } },
104
+ 'user' => User.new(email: 'pawel@example.com', name: 'Pawel'),
105
+ 'roles' => %w[admin manager user],
106
+ 'mixed' => { users: [User.new(email: 'max@example.com', name: 'Max', manager: true), User.new(email: 'pawel@example.com', name: 'Pawel', manager: false)] },
107
+ 'scores' => [ 10, 2, 5, 1000],
108
+ 'last_4_payments' => [
109
+ { 'amount' => 100, 'currency' => 'USD' },
110
+ { 'amount' => 200, 'currency' => 'USD' },
111
+ { 'amount' => 300, 'currency' => 'USD' },
112
+ { 'amount' => 400, 'currency' => 'USD' }
113
+ ],
114
+ 'array' => [
115
+ [1,2,3],
116
+ [4,5,6],
117
+ [
118
+ 7,
119
+ 8,
120
+ [':D']
121
+ ],
122
+ ]
123
+ }.extend(LightMapper).mapping(
124
+ 'source.google.search_word' => :word,
125
+ 'user.email' => :email,
126
+ 'user.as_json.name' => :name,
127
+ 'roles.0' => :first_role,
128
+ ['roles', 1] => :middle_role,
129
+ 'roles.last' => :last_role,
130
+ (->(source) { source[:mixed][:users].find { |user| user.manager }.email }) => :manager_email,
131
+ (->(source) { source[:mixed][:users].find { |user| user.manager }.name }) => :manager_name,
132
+ 'mixed.users.last.name' => :last_user_name,
133
+ (->(source) { source[:last_4_payments].map(&:values).map(&:first).max }) => :quarterly_payment_amount,
134
+ 'scores.sum' => :final_score,
135
+ 'array.2.2.first' => :smile
136
+ )
137
+ ```
138
+
139
+ result will be:
140
+
141
+ ```ruby
142
+ {
143
+ word: 'ruby',
144
+ email: 'pawel@example.com',
145
+ name: 'Pawel',
146
+ first_role: 'admin',
147
+ last_role: 'user',
148
+ manager_email: 'max@example.com',
149
+ manager_name: 'Max',
150
+ last_user_name: 'Pawel',
151
+ quarterly_payment_amount: 1000,
152
+ final_score: 1017
153
+ }
154
+ ```
155
+
156
+ ### Mappers selection via pattern matching
157
+
158
+ ```ruby
159
+ GOOGLE_MAPPER = { 'result.user.name' => :name }
160
+ LINKEDIN_MAPPER = { 'result.client.display_name' => :word }
161
+
162
+ data = { source: 'google', user: { name: 'test'}, 'result' => { 'user' => { 'name' => 'Pawel'} } }
163
+ mapper = case data
164
+ in source: 'google', user: {name:} then GOOGLE_MAPPER
165
+ in source: 'linkedin', client: {display_name:} then LINKEDIN_MAPPER
166
+ else
167
+ raise 'Unknown mapper'
168
+ end
169
+
170
+ data.extend(LightMapper).mapping(mapper)
171
+ # result { name: 'Pawel' }
172
+ ```
24
173
 
25
174
  ## Contributing
26
175
 
data/Rakefile CHANGED
@@ -1,2 +1,6 @@
1
1
  require 'bundler/gem_tasks'
2
+ require 'rspec/core/rake_task'
2
3
 
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -1,3 +1,3 @@
1
1
  module LightMapper
2
- VERSION = '0.0.1'
2
+ VERSION = '1.0.4'
3
3
  end
data/lib/light_mapper.rb CHANGED
@@ -1,16 +1,78 @@
1
1
  require 'light_mapper/version'
2
2
 
3
3
  module LightMapper
4
+ InvalidKey = Class.new(StandardError)
5
+ KeyMissing = Class.new(StandardError)
6
+
7
+ module Helper
8
+ def self.raise_key_missing(current, full_path, additional_message = nil)
9
+
10
+ raise KeyMissing, ["#{current} key not found; Full path #{full_path.map(&:to_s).join('.')}", additional_message].compact.join('; ')
11
+ end
12
+
13
+ def self.key_destructor(value)
14
+ case value
15
+ in String
16
+ value.split('.')
17
+ in Symbol
18
+ [value]
19
+ in Array
20
+ value
21
+ else
22
+ raise InvalidKey, "Invalid key type: #{value.class}"
23
+ end
24
+ end
25
+
26
+ def self.value_extractor(object, current, path, full_path, strict = false, any_keys = false)
27
+ result = case object
28
+ in Hash
29
+ hash_key_extractor(object, current, full_path, strict, any_keys)
30
+ in Array
31
+ array_key_extractor(object, current, full_path, strict, any_keys)
32
+ in NilClass
33
+ nil
34
+ else
35
+ method_name = current.to_s.to_sym
36
+ object.respond_to?(method_name) ? object.send(method_name) : !strict ? nil : raise_key_missing(current, full_path)
37
+ end
38
+
39
+ path.compact.empty? ? result : value_extractor(result, path.first, path[1..-1], full_path, strict, any_keys)
40
+ end
41
+
42
+ def self.hash_key_extractor(object, current, full_path, strict, any_keys)
43
+ keys = any_keys ? [current, current.to_s, current.to_s.to_sym] : [current]
44
+ raise_key_missing(current, full_path) if strict && !keys.any? { |k| object.key?(k) }
45
+
46
+ object.values_at(*keys).compact.first
47
+ end
48
+
49
+ def self.array_key_extractor(object, current, full_path, strict, _any_keys)
50
+ index = current.to_s.match(/^(\d)+$/) ? current.to_i : nil
51
+
52
+ if index
53
+ raise_key_missing(current, full_path) if strict && index && object.size < index.next
54
+
55
+ object[index]
56
+ else
57
+ method_name = current.to_s.to_sym
58
+ raise_key_missing(current, full_path, "Array do not respond on #{method_name}") if strict && !object.respond_to?(method_name)
59
+
60
+ object.public_send(method_name)
61
+ end
62
+ end
63
+ end
64
+
4
65
  def mapping(mappings, opts = {})
5
66
  LightMapper.mapping(clone, mappings, opts)
6
67
  end
7
68
 
8
69
  def self.mapping(hash, mappings, opts = {})
9
- require_keys = opts[:require_keys] == true
70
+ strict, any_keys = opts.values_at(:strict, :any_keys)
71
+ mappings.each_with_object({}) do |(k, v), h|
72
+ next h[v] = k.call(hash) if k.is_a?(Proc)
10
73
 
11
- fetch_method = require_keys ? :fetch : :[]
12
- {}.tap do |h|
13
- mappings.each { |k, v| h[v] = hash.public_send(fetch_method, k) }
74
+ key_path = Helper.key_destructor(k)
75
+ h[v] = Helper.value_extractor(hash, key_path.first, key_path[1..-1], key_path, strict, any_keys)
14
76
  end
15
77
  end
16
78
  end
data/light_mapper.gemspec CHANGED
@@ -17,8 +17,9 @@ Gem::Specification.new do |spec|
17
17
  spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
18
18
  spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
19
19
  spec.require_paths = %w(lib)
20
+ spec.required_ruby_version = '>= 2.7'
20
21
 
21
- spec.add_development_dependency 'bundler', '~> 1.7'
22
+ spec.add_development_dependency 'bundler', '~> 2.3'
22
23
  spec.add_development_dependency 'rake', '~> 10.4'
23
24
  spec.add_development_dependency 'rspec', '~> 3.0'
24
25
  spec.add_development_dependency 'guard', '~> 2.12'
@@ -1,4 +1,17 @@
1
1
  require 'spec_helper'
2
+ class User
3
+ attr_accessor :name, :email, :manager
4
+
5
+ def initialize(name:, email:, manager: false)
6
+ @name = name
7
+ @email = email
8
+ @manager = manager
9
+ end
10
+
11
+ def as_json
12
+ { 'name' => name, 'email' => email, 'manager' => manager }
13
+ end
14
+ end
2
15
 
3
16
  describe LightMapper do
4
17
 
@@ -12,7 +25,7 @@ describe LightMapper do
12
25
  }
13
26
  end
14
27
 
15
- let(:maper) do
28
+ let(:mapper) do
16
29
  {
17
30
  'A' => :a,
18
31
  'b' => 'z'
@@ -20,7 +33,7 @@ describe LightMapper do
20
33
  end
21
34
 
22
35
  it 'return correct hash' do
23
- expect(subject.mapping(maper)).to eq(a: original_hash['A'], 'z' => original_hash['b'])
36
+ expect(subject.mapping(mapper)).to eq(a: original_hash['A'], 'z' => original_hash['b'])
24
37
  end
25
38
  end
26
39
 
@@ -32,7 +45,7 @@ describe LightMapper do
32
45
  }
33
46
  end
34
47
 
35
- let(:maper) do
48
+ let(:mapper) do
36
49
  {
37
50
  'A' => :a,
38
51
  'c' => 'z'
@@ -40,7 +53,166 @@ describe LightMapper do
40
53
  end
41
54
 
42
55
  it 'raise KeyError when required key missing' do
43
- expect { subject.mapping(maper, require_keys: true) }.to raise_error(KeyError)
56
+ expect { subject.mapping(mapper, strict: true) }.to raise_error(LightMapper::KeyMissing)
57
+ end
58
+ end
59
+
60
+ describe 'basic data mapping when any keys kind is allowed' do
61
+ let(:original_hash) do
62
+ {
63
+ 'A' => 'test',
64
+ 'b' => 1,
65
+ c: 10
66
+
67
+ }
68
+ end
69
+
70
+ let(:mapper) do
71
+ {
72
+ 'A' => :a,
73
+ 'c' => :c,
74
+ b: 'z'
75
+ }
76
+ end
77
+
78
+ it 'return correct hash' do
79
+ expect(subject.mapping(mapper, any_keys: true)).to eq(
80
+ a: original_hash['A'],
81
+ c: original_hash[:c],
82
+ 'z' => original_hash['b']
83
+ )
84
+ end
85
+
86
+ describe 'raise KeyError' do
87
+ let(:mapper) do
88
+ {
89
+ 'A' => :a,
90
+ 'c' => :c,
91
+ k: 'z'
92
+ }
93
+ end
94
+
95
+ it ' when required key missing' do
96
+ expect { subject.mapping(mapper, strict: true, any_keys: true) }.to raise_error(LightMapper::KeyMissing)
97
+ end
98
+ end
99
+ end
100
+
101
+ describe 'more advanced mapping' do
102
+ let(:source) do
103
+ {
104
+ 'source' => { 'google' => { 'search_word' => 'ruby' } },
105
+ 'user' => User.new(email: 'pawel@example.com', name: 'Pawel'),
106
+ 'roles' => %w[admin manager user],
107
+ 'mixed' => { users: [User.new(email: 'max@example.com', name: 'Max', manager: true), User.new(email: 'pawel@example.com', name: 'Pawel', manager: false)] },
108
+ 'scores' => [ 10, 2, 5, 1000],
109
+ 'last_4_payments' => [
110
+ { 'amount' => 100, 'currency' => 'USD' },
111
+ { 'amount' => 200, 'currency' => 'USD' },
112
+ { 'amount' => 300, 'currency' => 'USD' },
113
+ { 'amount' => 400, 'currency' => 'USD' }
114
+ ],
115
+ 'array' => [
116
+ [1,2,3],
117
+ [4,5,6],
118
+ [
119
+ 7,
120
+ 8,
121
+ [':D']
122
+ ],
123
+ ]
124
+ }
125
+ end
126
+
127
+ context 'with nested hash mapping' do
128
+ let(:mapping) { {'source.google.search_word' => :word} }
129
+
130
+ it 'return correct result' do
131
+ expect(source.extend(LightMapper).mapping(mapping)).to eq(word: 'ruby')
132
+ end
133
+ end
134
+
135
+ context 'with nested array mapping' do
136
+ let(:mapping) { {'array.2.2.first' => :result} }
137
+
138
+ it 'return correct result' do
139
+ expect(source.extend(LightMapper).mapping(mapping)).to eq(result: ':D')
140
+ end
141
+ end
142
+
143
+ context 'with nested object mapping' do
144
+ let(:mapping) { {'user.email' => :result} }
145
+
146
+ it 'return correct result' do
147
+ expect(source.extend(LightMapper).mapping(mapping)).to eq(result: 'pawel@example.com')
148
+ end
149
+ end
150
+
151
+ context 'with mapping proc' do
152
+ let(:mapping) { { (->(source) { source['last_4_payments'].last['amount'].to_s }) => :result } }
153
+
154
+ it 'return correct result' do
155
+ expect(source.extend(LightMapper).mapping(mapping)).to eq(result: '400')
156
+ end
157
+ end
158
+
159
+ context 'with mix of nested object types' do
160
+ let(:mapping) do
161
+ {
162
+ 'source.google.search_word' => :word,
163
+ 'user.email' => :email,
164
+ 'user.as_json.name' => :name,
165
+ 'roles.0' => :first_role,
166
+ ['roles', 1] => :middle_role,
167
+ 'roles.last' => :last_role,
168
+ (->(source) { source['mixed'][:users].find { |user| user.manager }.email }) => :manager_email,
169
+ (->(source) { source['mixed'][:users].find { |user| user.manager }.name }) => :manager_name,
170
+ 'mixed.users.last.name' => :last_user_name,
171
+ (->(source) { source['last_4_payments'].map(&:values).map(&:first).max }) => :quarterly_payment_amount,
172
+ 'scores.sum' => :final_score,
173
+ 'array.2.2.first' => :smile
174
+ }
175
+ end
176
+
177
+ it 'return correct result' do
178
+ expect(source.extend(LightMapper).mapping(mapping, any_keys: true)).to match({
179
+ word: 'ruby',
180
+ email: 'pawel@example.com',
181
+ name: 'Pawel',
182
+ final_score: 1017,
183
+ first_role: 'admin',
184
+ last_role: 'user',
185
+ manager_email: 'max@example.com',
186
+ manager_name: 'Max',
187
+ last_user_name: 'Pawel',
188
+ middle_role: 'manager',
189
+ quarterly_payment_amount: 400,
190
+ smile: ':D'
191
+ })
192
+ end
193
+
194
+ it 'return correct result base on proper key types' do
195
+ expect(source.extend(LightMapper).mapping(mapping)).to match({
196
+ word: 'ruby',
197
+ email: 'pawel@example.com',
198
+ name: 'Pawel',
199
+ final_score: 1017,
200
+ first_role: 'admin',
201
+ last_role: 'user',
202
+ manager_email: 'max@example.com',
203
+ manager_name: 'Max',
204
+ last_user_name: nil,
205
+ middle_role: 'manager',
206
+ quarterly_payment_amount: 400,
207
+ smile: ':D'
208
+ })
209
+ end
210
+
211
+ it 'raise KeyMissing error' do
212
+ expect { source.extend(LightMapper).mapping({'user.non_existence_method' => :result}, strict: true) }.to raise_error(LightMapper::KeyMissing)
213
+ expect { source.extend(LightMapper).mapping({'last_4_payments.4' => :result}, strict: true) }.to raise_error(LightMapper::KeyMissing)
214
+ expect { source.extend(LightMapper).mapping({'source.google.missing_key' => :result}, strict: true) }.to raise_error(LightMapper::KeyMissing)
215
+ end
44
216
  end
45
217
  end
46
218
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: light_mapper
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.1
4
+ version: 1.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Pawel Niemczyk
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-04-27 00:00:00.000000000 Z
11
+ date: 2022-12-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -16,14 +16,14 @@ dependencies:
16
16
  requirements:
17
17
  - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '1.7'
19
+ version: '2.3'
20
20
  type: :development
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '1.7'
26
+ version: '2.3'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -147,7 +147,7 @@ homepage: https://github.com/pniemczyk/light_mapper
147
147
  licenses:
148
148
  - MIT
149
149
  metadata: {}
150
- post_install_message:
150
+ post_install_message:
151
151
  rdoc_options: []
152
152
  require_paths:
153
153
  - lib
@@ -155,16 +155,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
155
155
  requirements:
156
156
  - - ">="
157
157
  - !ruby/object:Gem::Version
158
- version: '0'
158
+ version: '2.7'
159
159
  required_rubygems_version: !ruby/object:Gem::Requirement
160
160
  requirements:
161
161
  - - ">="
162
162
  - !ruby/object:Gem::Version
163
163
  version: '0'
164
164
  requirements: []
165
- rubyforge_project:
166
- rubygems_version: 2.4.5
167
- signing_key:
165
+ rubygems_version: 3.1.6
166
+ signing_key:
168
167
  specification_version: 4
169
168
  summary: Very light hash mapper
170
169
  test_files: