yahm 0.0.13 → 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +85 -0
- data/Gemfile +5 -0
- data/README.md +42 -74
- data/lib/yahm.rb +132 -1
- data/lib/yahm/mapping.rb +50 -51
- data/lib/yahm/version.rb +1 -1
- data/spec/spec_helper.rb +4 -1
- data/spec/yahm/mapping_spec.rb +64 -33
- data/spec/yahm_spec.rb +88 -3
- data/yahm.gemspec +3 -5
- metadata +14 -58
- data/lib/yahm/hash_mapper.rb +0 -20
- data/spec/yahm/hash_mapper_spec.rb +0 -88
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 2f91884e21f0dc68a1777f51cda708d53f053608
|
4
|
+
data.tar.gz: c3ad59e14872e528f9a19d3b203645dbc9f9d36e
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 9d57cb78535fd2ef77a611088ed6311d0932b406e27b366de9f9ddc7477837ca14c7bbfb3814f3ce7f36a430e808186fb088528addbc979e151bcd6e0c0933ae
|
7
|
+
data.tar.gz: a59e91fd750bd9640cee0e3d71bb989fa7a6b8fafdf0748f927f725373344ff7e03649b18e92d5133edac055992c379bc314d592b890d8b8dddaba9cc1afce54
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,85 @@
|
|
1
|
+
## 0.0.x => 0.1.x
|
2
|
+
|
3
|
+
### `Yahm::Mapping`
|
4
|
+
|
5
|
+
#### Constructor now takes a block directly
|
6
|
+
```ruby
|
7
|
+
|
8
|
+
require "yahm"
|
9
|
+
|
10
|
+
mapping = Yahm::Mapping.new do
|
11
|
+
map "/foo", to: "/bar"
|
12
|
+
end
|
13
|
+
```
|
14
|
+
|
15
|
+
#### Constructor takes a new option ```:context```, which is used for lambda/proc method resolution
|
16
|
+
```ruby
|
17
|
+
some_class = Class.new do
|
18
|
+
require "yahm"
|
19
|
+
|
20
|
+
def some_method(value)
|
21
|
+
value.to_s + "bar"
|
22
|
+
end
|
23
|
+
|
24
|
+
def mapping
|
25
|
+
@mapping ||= Yahm::Mapping.new(context: self) do
|
26
|
+
map "/foo", to: "/bar", processed_by: lambda { |v| some_method(v) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def apply_mapping_to(hash)
|
31
|
+
mapping.apply_to(hash)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
some_class.new.apply_mapping_to({foo: "foo"})) # => {:bar=>"foobar"}
|
36
|
+
```
|
37
|
+
|
38
|
+
#### Renamed `translate_hash` to `apply_to`
|
39
|
+
|
40
|
+
```ruby
|
41
|
+
|
42
|
+
require "yahm"
|
43
|
+
|
44
|
+
mapping = Yahm::Mapping.new do
|
45
|
+
map "/foo", to: "/bar"
|
46
|
+
end
|
47
|
+
|
48
|
+
# before
|
49
|
+
# mapping.translate_hash({foo:1})
|
50
|
+
|
51
|
+
# now
|
52
|
+
mapping.apply_to({foo:1})
|
53
|
+
```
|
54
|
+
|
55
|
+
#### Lambdas/Procs as to: option now take two parameters ```|value, result|```
|
56
|
+
|
57
|
+
```ruby
|
58
|
+
require "yahm"
|
59
|
+
|
60
|
+
Yahm::Mapping.new do
|
61
|
+
map "/source", to: lambda { |v, result| result.merge!(target: true) }
|
62
|
+
end
|
63
|
+
```
|
64
|
+
|
65
|
+
### Removed `Yahm::HashMapper`
|
66
|
+
|
67
|
+
The modul was removed, because mappings can be stored and executed with plain ruby class/instance methods without the need for a seperate modul to extend classes with.
|
68
|
+
|
69
|
+
```ruby
|
70
|
+
require "yahm"
|
71
|
+
|
72
|
+
class Foo
|
73
|
+
def initialize(hash)
|
74
|
+
@hash = hash
|
75
|
+
end
|
76
|
+
|
77
|
+
@mapping = Yahm::Mapping.new do
|
78
|
+
map "/foo", to: "/bar"
|
79
|
+
end
|
80
|
+
|
81
|
+
def apply_mapping_to(hash = @hash)
|
82
|
+
@mapping.apply_to(hash)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
```
|
data/Gemfile
CHANGED
data/README.md
CHANGED
@@ -18,7 +18,7 @@ Or install it yourself as:
|
|
18
18
|
|
19
19
|
## Dependencies
|
20
20
|
|
21
|
-
|
21
|
+
* thread (https://github.com/meh/ruby-thread) for threading support
|
22
22
|
|
23
23
|
## Usage
|
24
24
|
|
@@ -28,16 +28,12 @@ None
|
|
28
28
|
```ruby
|
29
29
|
require "yahm"
|
30
30
|
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
define_mapper :from_other_record do
|
35
|
-
map "/record/id", to: "/id"
|
36
|
-
end
|
31
|
+
mapping = Yahm::Mapping.new do
|
32
|
+
map "/foo", to: "/bar"
|
37
33
|
end
|
38
34
|
|
39
|
-
|
40
|
-
=> {
|
35
|
+
mapping.apply_to({foo: 1})
|
36
|
+
# => {:bar=>1}
|
41
37
|
```
|
42
38
|
|
43
39
|
### More advanced examples
|
@@ -46,90 +42,62 @@ Record.new.from_other_record({ ... })
|
|
46
42
|
require "yahm"
|
47
43
|
|
48
44
|
class Record
|
49
|
-
|
50
|
-
|
45
|
+
|
46
|
+
def initialize(hash)
|
47
|
+
@record = mapping.apply_to(hash)
|
48
|
+
end
|
51
49
|
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
50
|
+
private
|
51
|
+
|
52
|
+
def lookup_id(id)
|
53
|
+
# ...
|
54
|
+
end
|
55
|
+
|
56
|
+
def mapping
|
57
|
+
@mapping ||= Yahm::Mapping.new(context: self) do
|
58
|
+
map "/record/id", to: "/id", processed_by: lambda { |id| lookup_id(id) }
|
59
|
+
map "/record/count", to: "/count", processed_by: proc { |count| count.to_i }
|
60
|
+
map "/record/subject[0]", to: "/subjects/most_important_subject"
|
61
|
+
map "/record/languages[]", to: "/languages", force_array: true
|
62
|
+
map "/record/authors", to: "/authors", split_by: ";"
|
63
|
+
map "/record/version", to: "/version", default: 1
|
64
|
+
map "/record/label", to: lambda { |label, result| result[:label] = label }
|
65
|
+
end
|
59
66
|
end
|
60
67
|
end
|
61
68
|
|
62
|
-
Record.new
|
63
|
-
=> { :id => "...", :count => ..., :subjects => { :most_important_subject => "..."}, ... }
|
64
|
-
|
65
|
-
Record.translated_hash
|
69
|
+
Record.new({ ... })
|
66
70
|
=> { :id => "...", :count => ..., :subjects => { :most_important_subject => "..."}, ... }
|
67
71
|
```
|
72
|
+
### Options
|
68
73
|
|
69
|
-
####
|
74
|
+
#### `to: [string|lambda|proc]`
|
70
75
|
|
71
|
-
The `to` option of a mapping rule can either be a string, naming the path in the resultung hash or a proc, which is executed
|
76
|
+
The `to` option of a mapping rule can either be a string, naming the path in the resultung hash or a lambda/proc, which is executed within the created mapper. When a `context` option was supplied to `#new` or `apply_to` methods are also resolved against this scope.
|
72
77
|
|
73
|
-
|
74
|
-
require "yahm"
|
78
|
+
#### `default: [value]`
|
75
79
|
|
76
|
-
|
77
|
-
extend Yahm::HashMapper
|
78
|
-
|
79
|
-
define_mapper :from_other_record do
|
80
|
-
def funky_method(value)
|
81
|
-
@result[:is_funky] = true if funky?(value)
|
82
|
-
end
|
83
|
-
|
84
|
-
map "/record/count", to: "count"
|
85
|
-
map "/record/name", to: lambda { |v| funky_method(v) }
|
86
|
-
end
|
87
|
-
end
|
88
|
-
```
|
80
|
+
If a value would be `nil` and `default` is given, the `default` value is used instead of `nil`.
|
89
81
|
|
90
|
-
####
|
82
|
+
#### `force_array: [true|false]`
|
91
83
|
|
92
|
-
|
84
|
+
This ensures, that the value written in the result ist always a hash. Even when the result would be `nil`, an empty array is created.
|
93
85
|
|
86
|
+
#### `processed_by: [lambda|proc]`
|
94
87
|
|
95
|
-
|
96
|
-
require "yahm"
|
88
|
+
You can pass a lambda/proc as `processed_by` option. Inside of lambdas or procs, `self` corresponds to the context of the defined mapper. When a `context` option was supplied to `#new` or `apply_to` methods are also resolved against this scope.
|
97
89
|
|
98
|
-
|
99
|
-
extend Yahm::HashMapper
|
100
|
-
|
101
|
-
def process_bool_string(string)
|
102
|
-
string == "true" ? true : (string == "false" ? false : nil)
|
103
|
-
end
|
104
|
-
|
105
|
-
define_mapper :from_other_record do
|
106
|
-
def cheap?(price)
|
107
|
-
price.to_i < 20
|
108
|
-
end
|
109
|
-
|
110
|
-
map "/record/id", to: "id", processed_by: :to_i
|
111
|
-
map "/record/count", to: "count", processed_by: proc { |v| v.to_i }
|
112
|
-
map "/record/subject", to: "subjects", processed_by: :downcase
|
113
|
-
map "/record/price", to: "is_cheap", processed_by: lambda { |v| cheap?(v) }
|
114
|
-
map "/record/is_available", to: "available", processed_by: lambda { |v| _self.process_bool_string(v) }
|
115
|
-
end
|
116
|
-
end
|
117
|
-
```
|
90
|
+
#### `split_by: [string]`
|
118
91
|
|
119
|
-
|
92
|
+
If a value is a string and `split_by` is supplied, the string is splitted by the given character and an array would be returned.
|
120
93
|
|
121
|
-
|
94
|
+
#### `use_threads: [true|false]` [experimental]
|
122
95
|
|
123
|
-
|
96
|
+
Use a thread pool to execute to rules of a mapping. The current implementation is very naiv. No locking of shared data etc. Use this *only* if *you know* what you are doing and if your rules a complex enough to compensate the overhead introduced by threading.
|
124
97
|
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
define_mapper :my_private_mapper, private: true do
|
129
|
-
map "/some/internals", to: "internals"
|
130
|
-
end
|
131
|
-
end
|
132
|
-
```
|
98
|
+
#### `thread_pool_size: [number]` [experimental]
|
99
|
+
|
100
|
+
The number of threads used to execute rules if `use_threads: true`.
|
133
101
|
|
134
102
|
## Related work
|
135
103
|
|
data/lib/yahm.rb
CHANGED
@@ -1,6 +1,137 @@
|
|
1
1
|
require "yahm/version"
|
2
2
|
|
3
3
|
class Yahm
|
4
|
-
require "yahm/hash_mapper"
|
5
4
|
require "yahm/mapping"
|
5
|
+
|
6
|
+
def self.get(object, path)
|
7
|
+
current_path_element, remaining_path = split_path(path)
|
8
|
+
|
9
|
+
if hash_path?(current_path_element) && object.is_a?(Hash)
|
10
|
+
key = current_path_element[1..-1]
|
11
|
+
|
12
|
+
# handle cases where one forces symbol/string keys
|
13
|
+
value =
|
14
|
+
if key.start_with?(":")
|
15
|
+
object[key[1..-1].to_sym]
|
16
|
+
elsif key.start_with?('"') && key.end_with?('"')
|
17
|
+
object[key[1..-2]]
|
18
|
+
else
|
19
|
+
object[key.to_sym] || object[key.to_s]
|
20
|
+
end
|
21
|
+
|
22
|
+
unless value.nil?
|
23
|
+
remaining_path ? self.get(value, remaining_path) : value
|
24
|
+
else
|
25
|
+
value
|
26
|
+
end
|
27
|
+
elsif array_path?(current_path_element) && object.is_a?(Array)
|
28
|
+
index_parameter = current_path_element.slice(1..-2)
|
29
|
+
|
30
|
+
index =
|
31
|
+
if index_parameter == ""
|
32
|
+
0..object.length
|
33
|
+
else
|
34
|
+
index_parameter.to_i
|
35
|
+
end
|
36
|
+
|
37
|
+
if remaining_path
|
38
|
+
unless object[index].nil?
|
39
|
+
map_result = object[index.is_a?(Range) ? index : index..index].map do |array_element|
|
40
|
+
self.get(array_element, remaining_path)
|
41
|
+
end.compact
|
42
|
+
|
43
|
+
map_result == [] ? nil : map_result.flatten(array_depth(map_result) - 1)
|
44
|
+
else
|
45
|
+
nil
|
46
|
+
end
|
47
|
+
else
|
48
|
+
object[index]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def self.set(object, path, value)
|
54
|
+
current_path_element, remaining_path = split_path(path)
|
55
|
+
next_path_element = split_path(remaining_path)[0]
|
56
|
+
|
57
|
+
if hash_path?(current_path_element) && object.is_a?(Hash)
|
58
|
+
key = current_path_element[1..-1]
|
59
|
+
|
60
|
+
# handle cases where one forces symbol/string keys
|
61
|
+
key =
|
62
|
+
if key.start_with?(":")
|
63
|
+
key[1..-1].to_sym
|
64
|
+
elsif key.start_with?('"') && key.end_with?('"')
|
65
|
+
key[1..-2]
|
66
|
+
else
|
67
|
+
key.to_sym # keys should be symbols per default
|
68
|
+
end
|
69
|
+
|
70
|
+
if next_path_element.nil?
|
71
|
+
object[key] = value
|
72
|
+
elsif hash_path?(next_path_element)
|
73
|
+
self.set(object[key] || object[key] = {}, remaining_path, value)
|
74
|
+
elsif array_path?(next_path_element)
|
75
|
+
self.set(object[key] || object[key] = [], remaining_path, value)
|
76
|
+
end
|
77
|
+
elsif array_path?(current_path_element) && object.is_a?(Array)
|
78
|
+
if hash_path?(next_path_element)
|
79
|
+
object.push(_object = {})
|
80
|
+
self.set(_object, remaining_path, value)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
#
|
86
|
+
private
|
87
|
+
#
|
88
|
+
def self.array_depth(array)
|
89
|
+
depth = 1
|
90
|
+
until array == (flattend_array = array.flatten(1))
|
91
|
+
depth += 1
|
92
|
+
array = flattend_array
|
93
|
+
end
|
94
|
+
|
95
|
+
depth
|
96
|
+
end
|
97
|
+
|
98
|
+
def self.array_path?(path)
|
99
|
+
path[0] == "["
|
100
|
+
end
|
101
|
+
|
102
|
+
def decode_string_or_symbol_from(string)
|
103
|
+
if string.start_with?(":")
|
104
|
+
string[1..-1].to_sym
|
105
|
+
elsif string.start_with?('"') && string.end_with?('"')
|
106
|
+
string[1..-2]
|
107
|
+
else
|
108
|
+
string.to_sym # defaults to symbols
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
def self.hash_path?(path)
|
113
|
+
path[0] == "/"
|
114
|
+
end
|
115
|
+
|
116
|
+
def self.split_path(path)
|
117
|
+
unless path.nil?
|
118
|
+
path_elements = path
|
119
|
+
.gsub(/(\/)|(\[)/,
|
120
|
+
"/" => " /",
|
121
|
+
"[" => " [",
|
122
|
+
)
|
123
|
+
.split(" ")
|
124
|
+
|
125
|
+
remaining_path =
|
126
|
+
if path_elements.length > 1
|
127
|
+
path_elements.slice(1..-1).join
|
128
|
+
else
|
129
|
+
nil
|
130
|
+
end
|
131
|
+
|
132
|
+
[path_elements.first, remaining_path]
|
133
|
+
else
|
134
|
+
[nil, nil]
|
135
|
+
end
|
136
|
+
end
|
6
137
|
end
|
data/lib/yahm/mapping.rb
CHANGED
@@ -1,76 +1,75 @@
|
|
1
|
+
require "thread/pool"
|
2
|
+
|
1
3
|
class Yahm::Mapping
|
2
4
|
|
3
|
-
|
5
|
+
attr_accessor :thread_pool_size
|
6
|
+
attr_accessor :use_threads
|
4
7
|
|
5
8
|
def initialize(options = {}, &block)
|
9
|
+
@context = options[:context]
|
6
10
|
@rules = []
|
7
11
|
@result = nil
|
8
|
-
|
9
|
-
|
12
|
+
@use_threads = options[:use_threads] || false
|
13
|
+
@thread_pool_size = options[:thread_pool_size] || 2
|
10
14
|
|
11
|
-
|
12
|
-
@rules.push [str, options]
|
13
|
-
self
|
15
|
+
self.instance_eval(&block) if block
|
14
16
|
end
|
15
17
|
|
16
|
-
def
|
17
|
-
@_self = options[:_self]
|
18
|
+
def apply_to(input_hash, options = {})
|
18
19
|
@result = {}
|
19
20
|
|
20
|
-
@
|
21
|
-
|
21
|
+
if @use_threads
|
22
|
+
thread_pool = Thread.pool(@thread_pool_size) if @use_threads
|
23
|
+
|
24
|
+
@rules.each do |rule|
|
25
|
+
thread_pool.process do
|
26
|
+
apply_rule(rule, input_hash, @result)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
thread_pool.shutdown
|
31
|
+
else
|
32
|
+
@rules.each do |rule|
|
33
|
+
apply_rule(rule, input_hash, @result)
|
34
|
+
end
|
22
35
|
end
|
23
36
|
|
24
37
|
@result
|
25
38
|
end
|
26
39
|
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
.split("/")
|
36
|
-
.map do |element|
|
37
|
-
if element[/\A\d+\Z/]
|
38
|
-
element.to_i
|
39
|
-
else
|
40
|
-
element.to_sym
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
if (to_option = rule.last[:to]).is_a?(String)
|
45
|
-
sanitized_target_path = rule.last[:to].sub(/^\//, "").split("/").map(&:to_sym)
|
46
|
-
target_parent_path = sanitized_target_path.slice(0, sanitized_target_path.length - 1)
|
40
|
+
#
|
41
|
+
# dynamic context method resolutuon
|
42
|
+
#
|
43
|
+
def method_missing(sym, *args, &block)
|
44
|
+
if @context.respond_to?(sym)
|
45
|
+
@context.send(sym, *args, &block)
|
46
|
+
else
|
47
|
+
super
|
47
48
|
end
|
49
|
+
end
|
48
50
|
|
49
|
-
|
51
|
+
def respond_to_missing?(sym, *)
|
52
|
+
@context.respond_to?(sym) || super
|
53
|
+
end
|
50
54
|
|
51
|
-
|
52
|
-
source_value = if symbol_or_callable.is_a?(Symbol)
|
53
|
-
source_value.send(symbol_or_callable)
|
54
|
-
else
|
55
|
-
symbol_or_callable.call(source_value)
|
56
|
-
end
|
57
|
-
end
|
55
|
+
private
|
58
56
|
|
59
|
-
|
60
|
-
|
61
|
-
|
57
|
+
def map(path, options = {})
|
58
|
+
@rules.push({ path: path }.merge(options))
|
59
|
+
self
|
60
|
+
end
|
62
61
|
|
63
|
-
|
64
|
-
|
65
|
-
|
62
|
+
def apply_rule(rule, input_hash, output_hash)
|
63
|
+
value = Yahm.get(input_hash, rule[:path])
|
64
|
+
value = rule[:default] && value.nil? ? rule[:default] : value
|
65
|
+
value = rule[:force_array] ? (value.nil? ? [] : [value].flatten(1)) : value
|
66
|
+
value = rule[:split_by] && value.is_a?(String) ? value.split(rule[:split_by]).map!(&:strip) : value
|
67
|
+
value = rule[:processed_by] ? rule[:processed_by].call(value) : value
|
66
68
|
|
67
|
-
if
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
})
|
72
|
-
elsif to_option.is_a?(Proc)
|
73
|
-
instance_exec(source_value, &to_option)
|
69
|
+
if rule[:to].is_a?(Proc)
|
70
|
+
instance_exec(value, output_hash, &rule[:to])
|
71
|
+
else
|
72
|
+
Yahm.set(output_hash, rule[:to], value)
|
74
73
|
end
|
75
74
|
end
|
76
75
|
end
|
data/lib/yahm/version.rb
CHANGED
data/spec/spec_helper.rb
CHANGED
data/spec/yahm/mapping_spec.rb
CHANGED
@@ -1,32 +1,54 @@
|
|
1
1
|
require_relative "../spec_helper"
|
2
2
|
|
3
3
|
describe Yahm::Mapping do
|
4
|
-
|
5
|
-
|
4
|
+
describe "#new" do
|
5
|
+
it "can be called with a block, containing the mapping described by the DSL" do
|
6
|
+
mapping = Yahm::Mapping.new do
|
7
|
+
map "/foo", to: "/bar"
|
8
|
+
end
|
9
|
+
|
10
|
+
expect(mapping.apply_to({foo:1})).to eq({:bar=>1})
|
11
|
+
end
|
12
|
+
|
13
|
+
context "when called with a \"context\" option" do
|
14
|
+
it "passes methods from withing the mapping (e.g. procs/lambdas) to the context" do
|
15
|
+
some_class = Class.new do
|
16
|
+
require "yahm"
|
6
17
|
|
7
|
-
|
8
|
-
|
18
|
+
def some_method(value)
|
19
|
+
value.to_s + "bar"
|
20
|
+
end
|
9
21
|
|
10
|
-
|
11
|
-
|
12
|
-
|
22
|
+
def mapping
|
23
|
+
@mapping ||= Yahm::Mapping.new(context: self) do
|
24
|
+
map "/foo", to: "/bar", processed_by: lambda { |v| some_method(v) }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def apply_mapping_to(hash)
|
29
|
+
mapping.apply_to(hash)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
expect(some_class.new.apply_mapping_to({foo: "foo"})).to eq({:bar=>"foobar"})
|
34
|
+
end
|
13
35
|
end
|
14
36
|
end
|
15
|
-
|
16
|
-
describe ".
|
37
|
+
|
38
|
+
describe ".apply_to" do
|
17
39
|
let(:mapping) do
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
40
|
+
Yahm::Mapping.new do
|
41
|
+
map "/record_id", to: "/id"
|
42
|
+
map "/record/title", to: "/title"
|
43
|
+
map "/record/isbns", to: "/my_data/isbns"
|
44
|
+
map "/record/isbns[1]", to: "/main_isbn" # when an array, one can specifiy which element to choose
|
45
|
+
map "/record/count", to: "/count", processed_by: lambda { |v| v.to_i }
|
46
|
+
map "/record/languages", to: "/languages", force_array: true
|
47
|
+
map "/record/authors", to: "/authors", split_by: ";"
|
48
|
+
map "/record/version", to: "/version", default: 1
|
49
|
+
map "/record/creators", to: "/creators", force_array: true # when source value is nil, there should be an empty array in the target hash
|
50
|
+
map "/record/publishers", to: "/publishers", split_by: ";" # if there are is no source value, this should result to nil in the target hash
|
51
|
+
end
|
30
52
|
end
|
31
53
|
|
32
54
|
let(:input_hash) do
|
@@ -45,10 +67,10 @@ describe Yahm::Mapping do
|
|
45
67
|
}
|
46
68
|
end
|
47
69
|
|
48
|
-
|
49
|
-
|
70
|
+
let(:expected_result) do
|
71
|
+
{
|
50
72
|
:id=>"some_id123",
|
51
|
-
:
|
73
|
+
:title=>"some title",
|
52
74
|
:my_data=>{:isbns=>["3-86680-192-0", "3-680-08783-7"]},
|
53
75
|
:main_isbn=>"3-680-08783-7",
|
54
76
|
:count=>3,
|
@@ -57,48 +79,57 @@ describe Yahm::Mapping do
|
|
57
79
|
:version=>1,
|
58
80
|
:creators=>[],
|
59
81
|
:publishers=>nil
|
60
|
-
}
|
82
|
+
}
|
83
|
+
end
|
84
|
+
|
85
|
+
it "translates a given hash according to @rules" do
|
86
|
+
expect(mapping.apply_to(input_hash)).to eq(expected_result)
|
61
87
|
end
|
62
88
|
|
89
|
+
context "when threading is enabled" do
|
90
|
+
it "translates a given hash according to @rules" do
|
91
|
+
mapping.use_threads = true
|
92
|
+
expect(mapping.apply_to(input_hash)).to eq(expected_result)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
63
96
|
it "does not apply \"split_by\" if source value is an array" do
|
64
|
-
expect(Yahm::Mapping.new
|
97
|
+
expect(Yahm::Mapping.new { map "/source/value", to: "/target", split_by: ";" }.apply_to(source: { value: ["foo", "foo;bar"] }))
|
65
98
|
.to eq({
|
66
99
|
:target => ["foo", "foo;bar"]
|
67
100
|
})
|
68
101
|
end
|
69
102
|
|
70
103
|
it "processes the source value by the lambda/proc given by \"processed_by\"" do
|
71
|
-
expect(Yahm::Mapping.new
|
104
|
+
expect(Yahm::Mapping.new { map "/source/value", to: "/target", processed_by: lambda { |v| v.downcase } }.apply_to(source: { value: "A STRING" }))
|
72
105
|
.to eq({
|
73
106
|
:target => "a string"
|
74
107
|
})
|
75
108
|
|
76
|
-
expect(Yahm::Mapping.new
|
109
|
+
expect(Yahm::Mapping.new { map "/source/value", to: "/target", processed_by: proc { |v| v.downcase } }.apply_to(source: { value: "A STRING" }))
|
77
110
|
.to eq({
|
78
111
|
:target => "a string"
|
79
112
|
})
|
80
113
|
end
|
81
114
|
|
82
115
|
it "returns nil if the source path does not exist" do
|
83
|
-
expect(Yahm::Mapping.new
|
84
|
-
.to eq({
|
116
|
+
expect(Yahm::Mapping.new { map "/source/value/min", to: "/target" }.apply_to(foo: "bar")).to eq({
|
85
117
|
:target => nil
|
86
118
|
})
|
87
119
|
end
|
88
120
|
|
89
121
|
it "accepts a lambda/proc as :to option" do
|
90
|
-
expect(Yahm::Mapping.new
|
122
|
+
expect(Yahm::Mapping.new { map "/source", to: lambda { |v, result| result.merge!(target: true) } }.apply_to(foo: "bar"))
|
91
123
|
.to eq({
|
92
124
|
:target => true
|
93
125
|
})
|
94
126
|
end
|
95
127
|
|
96
128
|
it "provides multiple values as an array of values to lambdas/procs" do
|
97
|
-
expect(Yahm::Mapping.new
|
129
|
+
expect(Yahm::Mapping.new { map "/source", to: "/array_length", processed_by: lambda { |v| v.length } }.apply_to(source: ["bar", "moep"]))
|
98
130
|
.to eq({
|
99
131
|
:array_length => 2
|
100
132
|
})
|
101
133
|
end
|
102
134
|
end
|
103
|
-
|
104
135
|
end
|
data/spec/yahm_spec.rb
CHANGED
@@ -1,8 +1,93 @@
|
|
1
1
|
require_relative "./spec_helper"
|
2
2
|
|
3
3
|
describe Yahm do
|
4
|
-
it "has a
|
5
|
-
expect(Yahm.const_defined? :
|
6
|
-
expect(Yahm::
|
4
|
+
it "has a class named Mapping" do
|
5
|
+
expect(Yahm.const_defined? :Mapping).to be_true
|
6
|
+
expect(Yahm::Mapping.class.is_a? Class)
|
7
|
+
end
|
8
|
+
|
9
|
+
describe "#get" do
|
10
|
+
let(:object) do
|
11
|
+
{
|
12
|
+
hello: "world",
|
13
|
+
empty_array: [],
|
14
|
+
empty_hash: [],
|
15
|
+
"string_key" => "foobar",
|
16
|
+
some: {
|
17
|
+
really: [
|
18
|
+
{
|
19
|
+
nested: [
|
20
|
+
{
|
21
|
+
structure: "hello"
|
22
|
+
},
|
23
|
+
{
|
24
|
+
structure: "world"
|
25
|
+
}
|
26
|
+
]
|
27
|
+
}
|
28
|
+
]
|
29
|
+
},
|
30
|
+
foo: [
|
31
|
+
{
|
32
|
+
bar: 1
|
33
|
+
},
|
34
|
+
{
|
35
|
+
bar: 2
|
36
|
+
}
|
37
|
+
],
|
38
|
+
arr_of_arrs: [
|
39
|
+
[ [1,2] , [3,4,5] ],
|
40
|
+
[ 6,7 ],
|
41
|
+
8
|
42
|
+
]
|
43
|
+
}
|
44
|
+
end
|
45
|
+
|
46
|
+
it "returns elements which match the given path" do
|
47
|
+
expect(Yahm.get(object, "/hello")).to eq("world")
|
48
|
+
expect(Yahm.get(object, "/empty_array")).to eq([])
|
49
|
+
expect(Yahm.get(object, "/empty_hash")).to eq([])
|
50
|
+
expect(Yahm.get(object, "/foo")).to eq([{:bar=>1}, {:bar=>2}])
|
51
|
+
expect(Yahm.get(object, "/foo[0]")).to eq({:bar=>1})
|
52
|
+
expect(Yahm.get(object, "/foo[1]")).to eq({:bar=>2})
|
53
|
+
expect(Yahm.get(object, "/foo[]/bar")).to eq([1,2])
|
54
|
+
expect(Yahm.get(object, "/arr_of_arrs[0][1][2]")).to eq([5])
|
55
|
+
expect(Yahm.get(object, "/some/really[]/nested[]/structure")).to eq(["hello", "world"])
|
56
|
+
expect(Yahm.get(object, "/arr_of_arrs[0]")).to eq([[1, 2], [3, 4, 5]])
|
57
|
+
end
|
58
|
+
|
59
|
+
it "returns nil if the path is invalid" do
|
60
|
+
expect(Yahm.get(object, "/hello_world")).to eq(nil)
|
61
|
+
expect(Yahm.get(object, "/foo[3]")).to eq(nil)
|
62
|
+
expect(Yahm.get(object, "/foo[]/bar/muff")).to eq(nil)
|
63
|
+
expect(Yahm.get(object, "/foo[]/bar[0]")).to eq(nil)
|
64
|
+
end
|
65
|
+
|
66
|
+
it "allows specification of the key type using : or \"\"" do
|
67
|
+
expect(Yahm.get(object, '/"string_key"')).to eq("foobar")
|
68
|
+
expect(Yahm.get(object, '/:string_key"')).to eq(nil)
|
69
|
+
expect(Yahm.get(object, "/string_key")).to eq("foobar")
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#set" do
|
74
|
+
it "sets the objects value according to path and value" do
|
75
|
+
Yahm.set(object = {}, "/foo[]/bar", [1,2,3])
|
76
|
+
expect(object).to eq({:foo=>[{:bar=>[1, 2, 3]}]})
|
77
|
+
end
|
78
|
+
|
79
|
+
it "allows specification of the key type using : or \"\"" do
|
80
|
+
Yahm.set(object = {}, "/:foo/\"bar\"", "foobar")
|
81
|
+
expect(object).to eq({:foo=>{"bar"=>"foobar"}})
|
82
|
+
end
|
83
|
+
|
84
|
+
it "extends existing arrays/hashes if present" do
|
85
|
+
Yahm.set(object = { foo: [{bar: [1,2,3]}] }, "/foo[]/bar", [4,5,6])
|
86
|
+
expect(object).to eq({:foo=>[{:bar=>[1, 2, 3]}, {:bar=>[4, 5, 6]}]})
|
87
|
+
|
88
|
+
Yahm.set(object = { foo: "bar" }, "/muff", "muffmuff")
|
89
|
+
expect(object).to eq({:foo=>"bar", :muff=>"muffmuff"})
|
90
|
+
end
|
91
|
+
|
7
92
|
end
|
8
93
|
end
|
data/yahm.gemspec
CHANGED
@@ -17,11 +17,9 @@ Gem::Specification.new do |spec|
|
|
17
17
|
spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
|
18
18
|
spec.require_paths = ["lib"]
|
19
19
|
|
20
|
+
spec.add_dependency "thread", ">= 0.1.4"
|
21
|
+
|
20
22
|
spec.add_development_dependency "bundler", "~> 1.5"
|
21
|
-
spec.add_development_dependency "
|
22
|
-
spec.add_development_dependency "pry-nav", "~> 0.2.3"
|
23
|
-
spec.add_development_dependency "pry-stack_explorer", "~> 0.4.9.1"
|
24
|
-
spec.add_development_dependency "pry-syntax-hacks", "~> 0.0.6"
|
25
|
-
spec.add_development_dependency "rspec", "~> 2.14.1"
|
23
|
+
spec.add_development_dependency "rspec", ">= 2.14.1"
|
26
24
|
spec.add_development_dependency "rake"
|
27
25
|
end
|
metadata
CHANGED
@@ -1,97 +1,55 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: yahm
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Michael Sievers
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2014-06-
|
11
|
+
date: 2014-06-03 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
|
-
name:
|
15
|
-
requirement: !ruby/object:Gem::Requirement
|
16
|
-
requirements:
|
17
|
-
- - "~>"
|
18
|
-
- !ruby/object:Gem::Version
|
19
|
-
version: '1.5'
|
20
|
-
type: :development
|
21
|
-
prerelease: false
|
22
|
-
version_requirements: !ruby/object:Gem::Requirement
|
23
|
-
requirements:
|
24
|
-
- - "~>"
|
25
|
-
- !ruby/object:Gem::Version
|
26
|
-
version: '1.5'
|
27
|
-
- !ruby/object:Gem::Dependency
|
28
|
-
name: pry
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: 0.9.12.4
|
34
|
-
type: :development
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: 0.9.12.4
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: pry-nav
|
43
|
-
requirement: !ruby/object:Gem::Requirement
|
44
|
-
requirements:
|
45
|
-
- - "~>"
|
46
|
-
- !ruby/object:Gem::Version
|
47
|
-
version: 0.2.3
|
48
|
-
type: :development
|
49
|
-
prerelease: false
|
50
|
-
version_requirements: !ruby/object:Gem::Requirement
|
51
|
-
requirements:
|
52
|
-
- - "~>"
|
53
|
-
- !ruby/object:Gem::Version
|
54
|
-
version: 0.2.3
|
55
|
-
- !ruby/object:Gem::Dependency
|
56
|
-
name: pry-stack_explorer
|
14
|
+
name: thread
|
57
15
|
requirement: !ruby/object:Gem::Requirement
|
58
16
|
requirements:
|
59
|
-
- - "
|
17
|
+
- - ">="
|
60
18
|
- !ruby/object:Gem::Version
|
61
|
-
version: 0.4
|
62
|
-
type: :
|
19
|
+
version: 0.1.4
|
20
|
+
type: :runtime
|
63
21
|
prerelease: false
|
64
22
|
version_requirements: !ruby/object:Gem::Requirement
|
65
23
|
requirements:
|
66
|
-
- - "
|
24
|
+
- - ">="
|
67
25
|
- !ruby/object:Gem::Version
|
68
|
-
version: 0.4
|
26
|
+
version: 0.1.4
|
69
27
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
28
|
+
name: bundler
|
71
29
|
requirement: !ruby/object:Gem::Requirement
|
72
30
|
requirements:
|
73
31
|
- - "~>"
|
74
32
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
33
|
+
version: '1.5'
|
76
34
|
type: :development
|
77
35
|
prerelease: false
|
78
36
|
version_requirements: !ruby/object:Gem::Requirement
|
79
37
|
requirements:
|
80
38
|
- - "~>"
|
81
39
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
40
|
+
version: '1.5'
|
83
41
|
- !ruby/object:Gem::Dependency
|
84
42
|
name: rspec
|
85
43
|
requirement: !ruby/object:Gem::Requirement
|
86
44
|
requirements:
|
87
|
-
- - "
|
45
|
+
- - ">="
|
88
46
|
- !ruby/object:Gem::Version
|
89
47
|
version: 2.14.1
|
90
48
|
type: :development
|
91
49
|
prerelease: false
|
92
50
|
version_requirements: !ruby/object:Gem::Requirement
|
93
51
|
requirements:
|
94
|
-
- - "
|
52
|
+
- - ">="
|
95
53
|
- !ruby/object:Gem::Version
|
96
54
|
version: 2.14.1
|
97
55
|
- !ruby/object:Gem::Dependency
|
@@ -117,16 +75,15 @@ extra_rdoc_files: []
|
|
117
75
|
files:
|
118
76
|
- ".gitignore"
|
119
77
|
- ".rspec"
|
78
|
+
- CHANGELOG.md
|
120
79
|
- Gemfile
|
121
80
|
- LICENSE.txt
|
122
81
|
- README.md
|
123
82
|
- Rakefile
|
124
83
|
- lib/yahm.rb
|
125
|
-
- lib/yahm/hash_mapper.rb
|
126
84
|
- lib/yahm/mapping.rb
|
127
85
|
- lib/yahm/version.rb
|
128
86
|
- spec/spec_helper.rb
|
129
|
-
- spec/yahm/hash_mapper_spec.rb
|
130
87
|
- spec/yahm/mapping_spec.rb
|
131
88
|
- spec/yahm_spec.rb
|
132
89
|
- yahm.gemspec
|
@@ -156,6 +113,5 @@ specification_version: 4
|
|
156
113
|
summary: Yet another ruby hash mapper
|
157
114
|
test_files:
|
158
115
|
- spec/spec_helper.rb
|
159
|
-
- spec/yahm/hash_mapper_spec.rb
|
160
116
|
- spec/yahm/mapping_spec.rb
|
161
117
|
- spec/yahm_spec.rb
|
data/lib/yahm/hash_mapper.rb
DELETED
@@ -1,20 +0,0 @@
|
|
1
|
-
module Yahm::HashMapper
|
2
|
-
def define_mapper(mapper_method_name, options = {}, &block)
|
3
|
-
mapping = Yahm::Mapping.new(&block)
|
4
|
-
|
5
|
-
define_method mapper_method_name do |hash|
|
6
|
-
translated_hash = mapping.translate_hash(hash, _self: self)
|
7
|
-
unless (setter_name = options[:call_setter]).nil?
|
8
|
-
self.send(setter_name, translated_hash)
|
9
|
-
end
|
10
|
-
|
11
|
-
translated_hash
|
12
|
-
end
|
13
|
-
|
14
|
-
if options[:private]
|
15
|
-
private mapper_method_name.to_sym
|
16
|
-
end
|
17
|
-
|
18
|
-
self
|
19
|
-
end
|
20
|
-
end
|
@@ -1,88 +0,0 @@
|
|
1
|
-
require_relative "../spec_helper"
|
2
|
-
|
3
|
-
describe Yahm::HashMapper do
|
4
|
-
context "when extending a class" do
|
5
|
-
let(:class_extended_with_hash_mapper) do
|
6
|
-
class MyClass
|
7
|
-
extend Yahm::HashMapper
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
11
|
-
it "adds a class method called #define_mapper" do
|
12
|
-
expect(class_extended_with_hash_mapper.methods).to include(:define_mapper)
|
13
|
-
end
|
14
|
-
end
|
15
|
-
|
16
|
-
describe "#define_mapper" do
|
17
|
-
let(:class_extended_with_hash_mapper) do
|
18
|
-
Class.new do
|
19
|
-
extend Yahm::HashMapper
|
20
|
-
|
21
|
-
def method_of_extended_object(value)
|
22
|
-
true
|
23
|
-
end
|
24
|
-
|
25
|
-
define_mapper :my_mapper do
|
26
|
-
def mapper_local_method(value)
|
27
|
-
true
|
28
|
-
end
|
29
|
-
|
30
|
-
map "/source[0]", to: "target_1", processed_by: proc { |v| mapper_local_method(v) }
|
31
|
-
map "/source[1]", to: "target_2", processed_by: proc { |v| _self.method_of_extended_object(v) }
|
32
|
-
map "/source[2]", to: "target_3", processed_by: proc { |v| another_local_method(v) }
|
33
|
-
|
34
|
-
def another_local_method(value)
|
35
|
-
true
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
define_mapper :my_private_mapper, private: true do
|
40
|
-
end
|
41
|
-
end
|
42
|
-
end
|
43
|
-
|
44
|
-
it "adds a named mapper method to the instances of the extended class" do
|
45
|
-
expect(class_extended_with_hash_mapper.new).to respond_to(:my_mapper)
|
46
|
-
end
|
47
|
-
|
48
|
-
it "provides the context of the defined mapper as \"self\" for \"processed_by\" callables" do
|
49
|
-
expect(class_extended_with_hash_mapper.new.my_mapper({source: ["foo", "bar"]})).to eq({
|
50
|
-
target_1: true,
|
51
|
-
target_2: true,
|
52
|
-
target_3: true
|
53
|
-
})
|
54
|
-
end
|
55
|
-
|
56
|
-
it "provides the context of the extended object as \"_self\" for \"processed_by\" callables" do
|
57
|
-
expect(class_extended_with_hash_mapper.new.my_mapper({source: ["foo", "bar"]})).to eq({
|
58
|
-
target_1: true,
|
59
|
-
target_2: true,
|
60
|
-
target_3: true
|
61
|
-
})
|
62
|
-
end
|
63
|
-
|
64
|
-
context "when called with an option called 'call_setter'" do
|
65
|
-
let(:class_extended_with_hash_mapper) do
|
66
|
-
Class.new do
|
67
|
-
extend Yahm::HashMapper
|
68
|
-
|
69
|
-
attr_accessor :my_result
|
70
|
-
|
71
|
-
define_mapper :my_mapper, call_setter: :my_result= do
|
72
|
-
end
|
73
|
-
end
|
74
|
-
end
|
75
|
-
|
76
|
-
it "calls a method with the given name with the result of the mapping" do
|
77
|
-
(instance = class_extended_with_hash_mapper.new).my_mapper({})
|
78
|
-
expect(instance.my_result).not_to be_nil
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
context "when called with private: true" do
|
83
|
-
it "adds a private named mapper method to the instances of the extended class" do
|
84
|
-
expect(class_extended_with_hash_mapper.new.private_methods).to include(:my_private_mapper)
|
85
|
-
end
|
86
|
-
end
|
87
|
-
end
|
88
|
-
end
|