two-way-mapper 0.0.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.travis.yml +4 -4
- data/README.md +28 -5
- data/lib/two-way-mapper.rb +2 -0
- data/lib/two_way_mapper.rb +2 -0
- data/lib/two_way_mapper/map.rb +4 -0
- data/lib/two_way_mapper/mapping.rb +58 -32
- data/lib/two_way_mapper/node.rb +3 -1
- data/lib/two_way_mapper/node/active_record.rb +3 -1
- data/lib/two_way_mapper/node/{abstract.rb → base.rb} +18 -15
- data/lib/two_way_mapper/node/hash.rb +6 -2
- data/lib/two_way_mapper/node/object.rb +9 -5
- data/lib/two_way_mapper/railtie.rb +4 -2
- data/lib/two_way_mapper/rule.rb +55 -19
- data/lib/two_way_mapper/tools.rb +4 -2
- data/lib/two_way_mapper/version.rb +3 -1
- data/spec/map_spec.rb +3 -1
- data/spec/mapping_spec.rb +118 -30
- data/spec/node/active_record_spec.rb +6 -2
- data/spec/node/{abstract_spec.rb → base_spec.rb} +10 -5
- data/spec/node/hash_spec.rb +7 -3
- data/spec/node/object_spec.rb +4 -2
- data/spec/rule_spec.rb +263 -85
- data/spec/tools_spec.rb +8 -8
- data/two-way-mapper.gemspec +16 -15
- metadata +26 -27
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 27c5a5f04384aebefba51e9ec085042cdf6d1ca40ff7d82aafba522b3b8eb457
|
4
|
+
data.tar.gz: 52d3d15700603d35a82e12f53c64765c71053644238bbe02cb44e9666934b6f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7d1319a99792280e03ea43e6b8ecb294cb6da12d443e4b7f4a1211d4ef44515589de48307244330044baa666c4691f4816b7466ac56401ced959321afb263685
|
7
|
+
data.tar.gz: 4e99bb150c889eb4eafcb966ba7f3756cee4d64e63f2ec0b0040f19f2d830e75d1f433234018ca11a7506bb7b35a5962a74cf491b5a0166b3783bf60793f25b1
|
data/.travis.yml
CHANGED
data/README.md
CHANGED
@@ -38,7 +38,30 @@ TwoWayMapper.register :customer do |mapping|
|
|
38
38
|
end
|
39
39
|
```
|
40
40
|
|
41
|
-
|
41
|
+
For simple rules use
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
TwoWayMapper.register :customer do |mapping|
|
45
|
+
mapping.left :object # set left plugin to object
|
46
|
+
mapping.right :hash, stringify_keys: true # set right plugin to hash
|
47
|
+
|
48
|
+
mapping.rules(
|
49
|
+
'first_name' => 'FirstName',
|
50
|
+
'last_name' => 'LastName'
|
51
|
+
)
|
52
|
+
end
|
53
|
+
```
|
54
|
+
|
55
|
+
Mapping can be defined explicitly without registration
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
mapping = TwoWayMapper::Mapping.new
|
59
|
+
mapping.left :object
|
60
|
+
mapping.right :hash, stringify_keys: true
|
61
|
+
# ...
|
62
|
+
```
|
63
|
+
|
64
|
+
Once the mapping is defined it can be used to convert one object to another and vice versa
|
42
65
|
|
43
66
|
```ruby
|
44
67
|
Customer = Struct.new :first_name, :last_name, :gender
|
@@ -46,7 +69,7 @@ Customer = Struct.new :first_name, :last_name, :gender
|
|
46
69
|
customer = Customer.new
|
47
70
|
api_response = { 'FirstName' => 'Evee', 'LastName' => 'Fjord', 'sex' => 'female' }
|
48
71
|
|
49
|
-
TwoWayMapper[:customer].from_right_to_left
|
72
|
+
TwoWayMapper[:customer].from_right_to_left(customer, api_response)
|
50
73
|
puts customer.first_name # => 'Evee'
|
51
74
|
puts customer.last_name # => 'Fjord'
|
52
75
|
puts customer.gender # => 'F'
|
@@ -58,17 +81,17 @@ another_customer.first_name = 'Step'
|
|
58
81
|
another_customer.last_name = 'Bander'
|
59
82
|
another_customer.gender = 'M'
|
60
83
|
|
61
|
-
TwoWayMapper[:customer].from_left_to_right
|
84
|
+
TwoWayMapper[:customer].from_left_to_right(another_customer, request_data)
|
62
85
|
puts request_data # => { 'FirstName' => 'Step', 'LastName' => 'Bander', sex: 'male' }
|
63
86
|
```
|
64
87
|
|
65
|
-
|
88
|
+
In rails, mappings can be defined in `app/mappings` folder
|
66
89
|
|
67
90
|
### Available plugins
|
68
91
|
|
69
92
|
* hash
|
70
93
|
* object
|
71
|
-
* active_record (same as object
|
94
|
+
* active_record (same as `object`, but for keys like `user.email`, it builds `user` before updating `email` on write)
|
72
95
|
|
73
96
|
## Contributing
|
74
97
|
|
data/lib/two-way-mapper.rb
CHANGED
data/lib/two_way_mapper.rb
CHANGED
data/lib/two_way_mapper/map.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TwoWayMapper
|
2
4
|
class Map
|
3
5
|
delegate :[], to: :@maps
|
@@ -8,7 +10,9 @@ module TwoWayMapper
|
|
8
10
|
|
9
11
|
def register(name)
|
10
12
|
mapping = TwoWayMapper::Mapping.new
|
13
|
+
|
11
14
|
yield mapping if block_given?
|
15
|
+
|
12
16
|
@maps[name.to_sym] = mapping
|
13
17
|
end
|
14
18
|
end
|
@@ -1,56 +1,82 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TwoWayMapper
|
2
4
|
class Mapping
|
3
|
-
|
5
|
+
DIRECTIONS = [:left, :right].freeze
|
6
|
+
|
7
|
+
attr_reader :rules_list, :left_class, :left_options, :right_class, :right_options
|
4
8
|
|
5
9
|
def initialize
|
6
|
-
@
|
10
|
+
@rules_list = []
|
7
11
|
end
|
8
12
|
|
9
|
-
[
|
13
|
+
[DIRECTIONS, DIRECTIONS.reverse].each do |from, to|
|
10
14
|
class_eval <<-CODE, __FILE__, __LINE__ + 1
|
11
|
-
def #{
|
12
|
-
@#{
|
13
|
-
@#{
|
15
|
+
def #{from}(plugin, options = {})
|
16
|
+
@#{from}_class = node_class(plugin)
|
17
|
+
@#{from}_options = options
|
14
18
|
end
|
15
|
-
CODE
|
16
|
-
end
|
17
19
|
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
20
|
+
def #{from}_selectors(mappable: false)
|
21
|
+
rules_list.flat_map do |rule|
|
22
|
+
if mappable && rule.from_#{from}_to_#{to}_only?
|
23
|
+
[]
|
24
|
+
else
|
25
|
+
rule.#{from}_nodes.map(&:selector)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def from_#{from}_to_#{to}(left_obj, right_obj)
|
31
|
+
rules_list.each { |rule| rule.from_#{from}_to_#{to}(left_obj, right_obj) }
|
32
|
+
#{to}_obj
|
33
|
+
end
|
34
|
+
CODE
|
22
35
|
end
|
23
36
|
|
24
|
-
def rule(
|
25
|
-
raise '
|
26
|
-
raise '
|
37
|
+
def rule(left_selectors, right_selectors = {}, options = {})
|
38
|
+
raise 'left is not defined' unless left_class
|
39
|
+
raise 'right is not defined' unless right_class
|
27
40
|
|
28
41
|
opt = options.dup
|
29
42
|
left_opt = opt.delete(:left) || {}
|
30
43
|
right_opt = opt.delete(:right) || {}
|
31
44
|
|
32
|
-
if
|
33
|
-
raise ArgumentError if
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
45
|
+
if left_selectors.is_a?(Hash)
|
46
|
+
raise ArgumentError if left_selectors.count < 2
|
47
|
+
|
48
|
+
opt = left_selectors
|
49
|
+
left_selectors = opt.keys.first
|
50
|
+
left_opt.merge! opt.delete(left_selectors)
|
51
|
+
right_selectors = opt.keys.first
|
52
|
+
right_opt.merge!(opt.delete(right_selectors))
|
39
53
|
end
|
40
54
|
|
41
|
-
|
42
|
-
|
55
|
+
@rules_list << Rule.new(
|
56
|
+
build_nodes(left_selectors, left_class, left_options.merge(left_opt)),
|
57
|
+
build_nodes(right_selectors, right_class, right_options.merge(right_opt)),
|
58
|
+
opt
|
59
|
+
)
|
60
|
+
end
|
43
61
|
|
44
|
-
|
62
|
+
def rules(hash)
|
63
|
+
hash.each do |left_selector, right_selector|
|
64
|
+
rule(left_selector, right_selector)
|
65
|
+
end
|
45
66
|
end
|
46
67
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
68
|
+
private
|
69
|
+
|
70
|
+
def node_class(plugin)
|
71
|
+
TwoWayMapper::Node.const_get(plugin.to_s.camelize)
|
72
|
+
rescue NameError
|
73
|
+
raise NameError, 'Cannot find node'
|
74
|
+
end
|
75
|
+
|
76
|
+
def build_nodes(selectors, klass, opt)
|
77
|
+
Array(selectors).map do |selector|
|
78
|
+
klass.new(selector, opt)
|
79
|
+
end
|
54
80
|
end
|
55
81
|
end
|
56
82
|
end
|
data/lib/two_way_mapper/node.rb
CHANGED
@@ -1,8 +1,10 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TwoWayMapper
|
2
4
|
module Node
|
3
5
|
extend ActiveSupport::Autoload
|
4
6
|
|
5
|
-
autoload :
|
7
|
+
autoload :Base, 'two_way_mapper/node/base'
|
6
8
|
autoload :Hash, 'two_way_mapper/node/hash'
|
7
9
|
autoload :Object, 'two_way_mapper/node/object'
|
8
10
|
autoload :ActiveRecord, 'two_way_mapper/node/active_record'
|
@@ -1,6 +1,8 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TwoWayMapper
|
2
4
|
module Node
|
3
|
-
class
|
5
|
+
class Base
|
4
6
|
DIVIDER = '.'
|
5
7
|
|
6
8
|
attr_accessor :selector, :options
|
@@ -14,47 +16,48 @@ module TwoWayMapper
|
|
14
16
|
block = options[:stringify_keys] ? :to_s : :to_sym
|
15
17
|
block = block.to_proc
|
16
18
|
end
|
17
|
-
selector.split(DIVIDER).map
|
19
|
+
selector.split(DIVIDER).map(&block)
|
18
20
|
end
|
19
21
|
|
20
22
|
def read(source)
|
21
|
-
rewind_forward(source) { |
|
23
|
+
rewind_forward(source) { |_obj, _key| return nil }
|
22
24
|
end
|
23
25
|
|
24
|
-
def write(
|
26
|
+
def write(_obj, _value)
|
27
|
+
raise NotImplementedError
|
25
28
|
end
|
26
29
|
|
27
30
|
def writable?(current_value, new_value)
|
28
31
|
!options[:write_if] ||
|
29
|
-
|
30
|
-
|
32
|
+
!options[:write_if].respond_to?(:call) ||
|
33
|
+
options[:write_if].call(current_value, new_value)
|
31
34
|
end
|
32
35
|
|
33
36
|
private
|
34
37
|
|
35
38
|
def rewind_forward(obj, margin = 0)
|
36
39
|
to = -(1 + margin.to_i.abs)
|
40
|
+
|
37
41
|
keys[0..to].each do |key|
|
38
42
|
unless rewind_to?(obj, key)
|
39
|
-
|
40
|
-
yield obj, key
|
41
|
-
else
|
42
|
-
create_node obj, key
|
43
|
-
end
|
43
|
+
block_given? ? yield(obj, key) : create_node(obj, key)
|
44
44
|
end
|
45
|
-
obj = next_key
|
45
|
+
obj = next_key(obj, key)
|
46
46
|
end
|
47
47
|
|
48
48
|
obj
|
49
49
|
end
|
50
50
|
|
51
|
-
def rewind_to?(
|
51
|
+
def rewind_to?(_obj, _key)
|
52
|
+
raise NotImplementedError
|
52
53
|
end
|
53
54
|
|
54
|
-
def create_node(
|
55
|
+
def create_node(_obj, _key)
|
56
|
+
raise NotImplementedError
|
55
57
|
end
|
56
58
|
|
57
|
-
def next_key(
|
59
|
+
def next_key(_obj, _key)
|
60
|
+
raise NotImplementedError
|
58
61
|
end
|
59
62
|
end
|
60
63
|
end
|
@@ -1,10 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TwoWayMapper
|
2
4
|
module Node
|
3
|
-
class Hash <
|
5
|
+
class Hash < Base
|
4
6
|
def write(source, value)
|
5
7
|
rewinded = rewind_forward source, 1
|
6
8
|
|
7
|
-
|
9
|
+
return unless writable?(rewinded[keys.last], value)
|
10
|
+
|
11
|
+
rewinded[keys.last] = value
|
8
12
|
end
|
9
13
|
|
10
14
|
private
|
@@ -1,23 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TwoWayMapper
|
2
4
|
module Node
|
3
|
-
class Object <
|
5
|
+
class Object < Base
|
4
6
|
def write(source, value)
|
5
|
-
rewinded = rewind_forward
|
7
|
+
rewinded = rewind_forward(source, 1)
|
8
|
+
|
9
|
+
return unless writable?(rewinded.send(keys.last), value)
|
6
10
|
|
7
|
-
rewinded.send
|
11
|
+
rewinded.send("#{keys.last}=", value)
|
8
12
|
end
|
9
13
|
|
10
14
|
private
|
11
15
|
|
12
16
|
def rewind_to?(obj, key)
|
13
|
-
obj.respond_to?
|
17
|
+
obj.respond_to?(key)
|
14
18
|
end
|
15
19
|
|
16
20
|
def create_node(obj, key)
|
17
21
|
end
|
18
22
|
|
19
23
|
def next_key(obj, key)
|
20
|
-
obj.send
|
24
|
+
obj.send(key)
|
21
25
|
end
|
22
26
|
end
|
23
27
|
end
|
@@ -1,10 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'rails'
|
2
4
|
|
3
5
|
module TwoWayMapper
|
4
6
|
class Railtie < Rails::Railtie
|
5
|
-
initializer
|
7
|
+
initializer 'two_way_mapper.set_load_path' do |app|
|
6
8
|
path = Rails.root.join('app', 'mappings', '*.rb').to_s
|
7
|
-
Dir[path].each{ |file| load file }
|
9
|
+
Dir[path].each { |file| load file }
|
8
10
|
end
|
9
11
|
end
|
10
12
|
end
|
data/lib/two_way_mapper/rule.rb
CHANGED
@@ -1,36 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module TwoWayMapper
|
2
4
|
class Rule
|
3
|
-
attr_reader :
|
5
|
+
attr_reader :left_nodes, :right_nodes
|
4
6
|
|
5
|
-
def initialize(
|
6
|
-
@
|
7
|
-
@
|
7
|
+
def initialize(left_nodes, right_nodes, opt = {})
|
8
|
+
@left_nodes = left_nodes
|
9
|
+
@right_nodes = right_nodes
|
8
10
|
@options = opt
|
9
11
|
end
|
10
12
|
|
11
|
-
|
12
|
-
|
13
|
-
def from_#{from}_to_#{to}(left_obj, right_obj)
|
14
|
-
value = #{from}.read #{from}_obj
|
15
|
-
value = map_value value, #{(from == :left).inspect}
|
16
|
-
if @options[:on_#{from}_to_#{to}].respond_to? :call
|
17
|
-
value = @options[:on_#{from}_to_#{to}].call value
|
18
|
-
end
|
19
|
-
#{to}.write #{to}_obj, value
|
13
|
+
def from_left_to_right(left_obj, right_obj)
|
14
|
+
return right_obj if from_right_to_left_only?
|
20
15
|
|
21
|
-
|
22
|
-
|
23
|
-
|
16
|
+
value = read(left_nodes, [left_obj, right_obj], true)
|
17
|
+
|
18
|
+
write(right_nodes, right_obj, value)
|
19
|
+
end
|
20
|
+
|
21
|
+
def from_right_to_left(left_obj, right_obj)
|
22
|
+
return left_obj if from_left_to_right_only?
|
23
|
+
|
24
|
+
value = read(right_nodes, [left_obj, right_obj], false)
|
25
|
+
|
26
|
+
write(left_nodes, left_obj, value)
|
27
|
+
end
|
28
|
+
|
29
|
+
def from_right_to_left_only?
|
30
|
+
@options[:from_right_to_left_only]
|
31
|
+
end
|
32
|
+
|
33
|
+
def from_left_to_right_only?
|
34
|
+
@options[:from_left_to_right_only]
|
24
35
|
end
|
25
36
|
|
26
37
|
private
|
27
38
|
|
28
|
-
def
|
39
|
+
def read(nodes, objects, left_to_right)
|
40
|
+
callback = left_to_right ? :on_left_to_right : :on_right_to_left
|
41
|
+
obj = left_to_right ? objects.first : objects.last
|
42
|
+
value = nil
|
43
|
+
|
44
|
+
nodes.each do |node|
|
45
|
+
value = node.read(obj)
|
46
|
+
value = map_value(value, left_to_right)
|
47
|
+
if @options[callback].respond_to?(:call)
|
48
|
+
args = [value] + objects + [node]
|
49
|
+
value = @options[callback].call(*args)
|
50
|
+
end
|
51
|
+
|
52
|
+
break if value
|
53
|
+
end
|
54
|
+
|
55
|
+
value
|
56
|
+
end
|
57
|
+
|
58
|
+
def write(nodes, obj, value)
|
59
|
+
nodes.each { |node| node.write(obj, value) }
|
60
|
+
|
61
|
+
obj
|
62
|
+
end
|
63
|
+
|
64
|
+
def map_value(value, left_to_right)
|
29
65
|
map = @options[:map]
|
30
|
-
if map
|
66
|
+
if map.is_a?(Hash)
|
31
67
|
map = map.invert unless left_to_right
|
32
68
|
default_key = "default_#{left_to_right ? 'left' : 'right'}".to_sym
|
33
|
-
|
69
|
+
map[value] || @options[default_key] || @options[:default]
|
34
70
|
else
|
35
71
|
value
|
36
72
|
end
|