larrow-runner 0.0.1 → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/README.md +4 -5
- data/larrow-runner.gemspec +0 -3
- data/lib/active_support/core_ext/hash.rb +7 -0
- data/lib/active_support/core_ext/hash/compact.rb +20 -0
- data/lib/active_support/core_ext/hash/deep_merge.rb +38 -0
- data/lib/active_support/core_ext/hash/except.rb +15 -0
- data/lib/active_support/core_ext/hash/indifferent_access.rb +23 -0
- data/lib/active_support/core_ext/hash/keys.rb +162 -0
- data/lib/active_support/core_ext/hash/reverse_merge.rb +22 -0
- data/lib/active_support/core_ext/hash/slice.rb +42 -0
- data/lib/active_support/hash_with_indifferent_access.rb +272 -0
- data/lib/larrow/runner.rb +3 -3
- data/lib/larrow/runner/cli/tools.rb +5 -1
- data/lib/larrow/runner/errors.rb +2 -1
- data/lib/larrow/runner/logger.rb +2 -2
- data/lib/larrow/runner/manager.rb +10 -3
- data/lib/larrow/runner/manifest.rb +15 -4
- data/lib/larrow/runner/manifest/adapter/larrow.rb +28 -26
- data/lib/larrow/runner/manifest/adapter/travis.rb +53 -52
- data/lib/larrow/runner/manifest/base_loader.rb +18 -16
- data/lib/larrow/runner/manifest/configuration.rb +118 -116
- data/lib/larrow/runner/model/app.rb +4 -4
- data/lib/larrow/runner/model/node.rb +56 -56
- data/lib/larrow/runner/service/cloud.rb +10 -4
- data/lib/larrow/runner/session.rb +14 -11
- data/lib/larrow/runner/vcs/base.rb +14 -12
- data/lib/larrow/runner/vcs/github.rb +40 -38
- data/lib/larrow/runner/version.rb +1 -1
- metadata +11 -50
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 649d405f97c00eba29947141c7c23f97da18b783
|
4
|
+
data.tar.gz: ab1fe09db103b64cb52fcc1003cf3ba3e4e0faa4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0ca859ab0d4d0f713a0a3e86d0340ca1b91d76e941acfe97e4318ead3e09fe9347d99f65118a357629be6a045edbc907e23ac9055da03c0eb10933765524dc23
|
7
|
+
data.tar.gz: f85aaf2b37ddfd2b8e0ef2daed67bbc591b3c8b75343efbf0b8f1c8105f30505d678e9b4dbcb8170fe3df1e887b55ee73d5a0c96ae3f46f8277519039163e8bc
|
data/README.md
CHANGED
@@ -40,27 +40,26 @@ Congratulation! Now you can use larrow to help your develop works.
|
|
40
40
|
|
41
41
|
### testing
|
42
42
|
|
43
|
-
|
43
|
+
Unit test, integration test, system test, etc.
|
44
44
|
```
|
45
45
|
$ larrow go <source_url>
|
46
46
|
```
|
47
47
|
|
48
48
|
### application startup
|
49
49
|
|
50
|
-
|
50
|
+
Make a standalone application and start it(if necessary)
|
51
51
|
```
|
52
52
|
$ larrow build server <source_url>
|
53
53
|
```
|
54
54
|
|
55
55
|
### build image
|
56
56
|
|
57
|
-
|
57
|
+
Use image to speed-up your development
|
58
58
|
|
59
59
|
* build a image of your application
|
60
60
|
```
|
61
61
|
$ larrow build image <source_url>
|
62
62
|
```
|
63
|
-
|
64
63
|
* build a image from local LarrowFile
|
65
64
|
```
|
66
65
|
$ larrow build image <larrow_file_path>
|
@@ -70,7 +69,7 @@ $ larrow build image <larrow_file_path>
|
|
70
69
|
|
71
70
|
Larrow need to know how to setup/make/install/start... your application. So developer could write a `Larrow File` to declare these things.
|
72
71
|
|
73
|
-
|
72
|
+
default larrow file: $source/.larrow.yml
|
74
73
|
## Contributing
|
75
74
|
|
76
75
|
1. Fork it ( http://github.com/fsword/larrow-core/fork )
|
data/larrow-runner.gemspec
CHANGED
@@ -24,12 +24,9 @@ Gem::Specification.new do |spec|
|
|
24
24
|
spec.add_development_dependency "simplecov", '~> 0.9'
|
25
25
|
spec.add_development_dependency "parallel_tests", '~> 1.0'
|
26
26
|
|
27
|
-
spec.add_runtime_dependency 'tilt', '~> 2'
|
28
27
|
spec.add_runtime_dependency 'thor', '~> 0.19'
|
29
|
-
spec.add_runtime_dependency "activesupport", "~> 4.1"
|
30
28
|
spec.add_runtime_dependency "pry", '~> 0.10', '0.10.0'
|
31
29
|
spec.add_runtime_dependency "pry-nav", '~> 0.2', '0.2.4'
|
32
|
-
spec.add_runtime_dependency "minitest", '~> 5.4', '5.4.1'
|
33
30
|
|
34
31
|
spec.add_runtime_dependency 'net-ssh', '~> 2.9'
|
35
32
|
spec.add_runtime_dependency 'net-scp', '~> 1.2'
|
@@ -0,0 +1,7 @@
|
|
1
|
+
require 'active_support/core_ext/hash/compact'
|
2
|
+
require 'active_support/core_ext/hash/deep_merge'
|
3
|
+
require 'active_support/core_ext/hash/except'
|
4
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
5
|
+
require 'active_support/core_ext/hash/keys'
|
6
|
+
require 'active_support/core_ext/hash/reverse_merge'
|
7
|
+
require 'active_support/core_ext/hash/slice'
|
@@ -0,0 +1,20 @@
|
|
1
|
+
class Hash
|
2
|
+
# Returns a hash with non +nil+ values.
|
3
|
+
#
|
4
|
+
# hash = { a: true, b: false, c: nil}
|
5
|
+
# hash.compact # => { a: true, b: false}
|
6
|
+
# hash # => { a: true, b: false, c: nil}
|
7
|
+
# { c: nil }.compact # => {}
|
8
|
+
def compact
|
9
|
+
self.select { |_, value| !value.nil? }
|
10
|
+
end
|
11
|
+
|
12
|
+
# Replaces current hash with non +nil+ values.
|
13
|
+
#
|
14
|
+
# hash = { a: true, b: false, c: nil}
|
15
|
+
# hash.compact! # => { a: true, b: false}
|
16
|
+
# hash # => { a: true, b: false}
|
17
|
+
def compact!
|
18
|
+
self.reject! { |_, value| value.nil? }
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
class Hash
|
2
|
+
# Returns a new hash with +self+ and +other_hash+ merged recursively.
|
3
|
+
#
|
4
|
+
# h1 = { a: true, b: { c: [1, 2, 3] } }
|
5
|
+
# h2 = { a: false, b: { x: [3, 4, 5] } }
|
6
|
+
#
|
7
|
+
# h1.deep_merge(h2) #=> { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
|
8
|
+
#
|
9
|
+
# Like with Hash#merge in the standard library, a block can be provided
|
10
|
+
# to merge values:
|
11
|
+
#
|
12
|
+
# h1 = { a: 100, b: 200, c: { c1: 100 } }
|
13
|
+
# h2 = { b: 250, c: { c1: 200 } }
|
14
|
+
# h1.deep_merge(h2) { |key, this_val, other_val| this_val + other_val }
|
15
|
+
# # => { a: 100, b: 450, c: { c1: 300 } }
|
16
|
+
def deep_merge(other_hash, &block)
|
17
|
+
dup.deep_merge!(other_hash, &block)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Same as +deep_merge+, but modifies +self+.
|
21
|
+
def deep_merge!(other_hash, &block)
|
22
|
+
other_hash.each_pair do |current_key, other_value|
|
23
|
+
this_value = self[current_key]
|
24
|
+
|
25
|
+
self[current_key] = if this_value.is_a?(Hash) && other_value.is_a?(Hash)
|
26
|
+
this_value.deep_merge(other_value, &block)
|
27
|
+
else
|
28
|
+
if block_given? && key?(current_key)
|
29
|
+
block.call(current_key, this_value, other_value)
|
30
|
+
else
|
31
|
+
other_value
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
self
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Hash
|
2
|
+
# Returns a hash that includes everything but the given keys. This is useful for
|
3
|
+
# limiting a set of parameters to everything but a few known toggles:
|
4
|
+
#
|
5
|
+
# @person.update(params[:person].except(:admin))
|
6
|
+
def except(*keys)
|
7
|
+
dup.except!(*keys)
|
8
|
+
end
|
9
|
+
|
10
|
+
# Replaces the hash without the given keys.
|
11
|
+
def except!(*keys)
|
12
|
+
keys.each { |key| delete(key) }
|
13
|
+
self
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'active_support/hash_with_indifferent_access'
|
2
|
+
|
3
|
+
class Hash
|
4
|
+
|
5
|
+
# Returns an <tt>ActiveSupport::HashWithIndifferentAccess</tt> out of its receiver:
|
6
|
+
#
|
7
|
+
# { a: 1 }.with_indifferent_access['a'] # => 1
|
8
|
+
def with_indifferent_access
|
9
|
+
ActiveSupport::HashWithIndifferentAccess.new_from_hash_copying_default(self)
|
10
|
+
end
|
11
|
+
|
12
|
+
# Called when object is nested under an object that receives
|
13
|
+
# #with_indifferent_access. This method will be called on the current object
|
14
|
+
# by the enclosing object and is aliased to #with_indifferent_access by
|
15
|
+
# default. Subclasses of Hash may overwrite this method to return +self+ if
|
16
|
+
# converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
|
17
|
+
# desirable.
|
18
|
+
#
|
19
|
+
# b = { b: 1 }
|
20
|
+
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
|
21
|
+
# # => {"b"=>32}
|
22
|
+
alias nested_under_indifferent_access with_indifferent_access
|
23
|
+
end
|
@@ -0,0 +1,162 @@
|
|
1
|
+
class Hash
|
2
|
+
# Returns a new hash with all keys converted using the block operation.
|
3
|
+
#
|
4
|
+
# hash = { name: 'Rob', age: '28' }
|
5
|
+
#
|
6
|
+
# hash.transform_keys{ |key| key.to_s.upcase }
|
7
|
+
# # => {"NAME"=>"Rob", "AGE"=>"28"}
|
8
|
+
def transform_keys
|
9
|
+
result = {}
|
10
|
+
each_key do |key|
|
11
|
+
result[yield(key)] = self[key]
|
12
|
+
end
|
13
|
+
result
|
14
|
+
end
|
15
|
+
|
16
|
+
# Destructively convert all keys using the block operations.
|
17
|
+
# Same as transform_keys but modifies +self+.
|
18
|
+
def transform_keys!
|
19
|
+
keys.each do |key|
|
20
|
+
self[yield(key)] = delete(key)
|
21
|
+
end
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Returns a new hash with all keys converted to strings.
|
26
|
+
#
|
27
|
+
# hash = { name: 'Rob', age: '28' }
|
28
|
+
#
|
29
|
+
# hash.stringify_keys
|
30
|
+
# # => { "name" => "Rob", "age" => "28" }
|
31
|
+
def stringify_keys
|
32
|
+
transform_keys{ |key| key.to_s }
|
33
|
+
end
|
34
|
+
|
35
|
+
# Destructively convert all keys to strings. Same as
|
36
|
+
# +stringify_keys+, but modifies +self+.
|
37
|
+
def stringify_keys!
|
38
|
+
transform_keys!{ |key| key.to_s }
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
42
|
+
# they respond to +to_sym+.
|
43
|
+
#
|
44
|
+
# hash = { 'name' => 'Rob', 'age' => '28' }
|
45
|
+
#
|
46
|
+
# hash.symbolize_keys
|
47
|
+
# # => { name: "Rob", age: "28" }
|
48
|
+
def symbolize_keys
|
49
|
+
transform_keys{ |key| key.to_sym rescue key }
|
50
|
+
end
|
51
|
+
alias_method :to_options, :symbolize_keys
|
52
|
+
|
53
|
+
# Destructively convert all keys to symbols, as long as they respond
|
54
|
+
# to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
|
55
|
+
def symbolize_keys!
|
56
|
+
transform_keys!{ |key| key.to_sym rescue key }
|
57
|
+
end
|
58
|
+
alias_method :to_options!, :symbolize_keys!
|
59
|
+
|
60
|
+
# Validate all keys in a hash match <tt>*valid_keys</tt>, raising ArgumentError
|
61
|
+
# on a mismatch. Note that keys are NOT treated indifferently, meaning if you
|
62
|
+
# use strings for keys but assert symbols as keys, this will fail.
|
63
|
+
#
|
64
|
+
# { name: 'Rob', years: '28' }.assert_valid_keys(:name, :age) # => raises "ArgumentError: Unknown key: :years. Valid keys are: :name, :age"
|
65
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys('name', 'age') # => raises "ArgumentError: Unknown key: :name. Valid keys are: 'name', 'age'"
|
66
|
+
# { name: 'Rob', age: '28' }.assert_valid_keys(:name, :age) # => passes, raises nothing
|
67
|
+
def assert_valid_keys(*valid_keys)
|
68
|
+
valid_keys.flatten!
|
69
|
+
each_key do |k|
|
70
|
+
unless valid_keys.include?(k)
|
71
|
+
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{valid_keys.map(&:inspect).join(', ')}")
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
# Returns a new hash with all keys converted by the block operation.
|
77
|
+
# This includes the keys from the root hash and from all
|
78
|
+
# nested hashes and arrays.
|
79
|
+
#
|
80
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
81
|
+
#
|
82
|
+
# hash.deep_transform_keys{ |key| key.to_s.upcase }
|
83
|
+
# # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
|
84
|
+
def deep_transform_keys(&block)
|
85
|
+
_deep_transform_keys_in_object(self, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
# Destructively convert all keys by using the block operation.
|
89
|
+
# This includes the keys from the root hash and from all
|
90
|
+
# nested hashes and arrays.
|
91
|
+
def deep_transform_keys!(&block)
|
92
|
+
_deep_transform_keys_in_object!(self, &block)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Returns a new hash with all keys converted to strings.
|
96
|
+
# This includes the keys from the root hash and from all
|
97
|
+
# nested hashes and arrays.
|
98
|
+
#
|
99
|
+
# hash = { person: { name: 'Rob', age: '28' } }
|
100
|
+
#
|
101
|
+
# hash.deep_stringify_keys
|
102
|
+
# # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
|
103
|
+
def deep_stringify_keys
|
104
|
+
deep_transform_keys{ |key| key.to_s }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Destructively convert all keys to strings.
|
108
|
+
# This includes the keys from the root hash and from all
|
109
|
+
# nested hashes and arrays.
|
110
|
+
def deep_stringify_keys!
|
111
|
+
deep_transform_keys!{ |key| key.to_s }
|
112
|
+
end
|
113
|
+
|
114
|
+
# Returns a new hash with all keys converted to symbols, as long as
|
115
|
+
# they respond to +to_sym+. This includes the keys from the root hash
|
116
|
+
# and from all nested hashes and arrays.
|
117
|
+
#
|
118
|
+
# hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
|
119
|
+
#
|
120
|
+
# hash.deep_symbolize_keys
|
121
|
+
# # => {:person=>{:name=>"Rob", :age=>"28"}}
|
122
|
+
def deep_symbolize_keys
|
123
|
+
deep_transform_keys{ |key| key.to_sym rescue key }
|
124
|
+
end
|
125
|
+
|
126
|
+
# Destructively convert all keys to symbols, as long as they respond
|
127
|
+
# to +to_sym+. This includes the keys from the root hash and from all
|
128
|
+
# nested hashes and arrays.
|
129
|
+
def deep_symbolize_keys!
|
130
|
+
deep_transform_keys!{ |key| key.to_sym rescue key }
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
# support methods for deep transforming nested hashes and arrays
|
135
|
+
def _deep_transform_keys_in_object(object, &block)
|
136
|
+
case object
|
137
|
+
when Hash
|
138
|
+
object.each_with_object({}) do |(key, value), result|
|
139
|
+
result[yield(key)] = _deep_transform_keys_in_object(value, &block)
|
140
|
+
end
|
141
|
+
when Array
|
142
|
+
object.map {|e| _deep_transform_keys_in_object(e, &block) }
|
143
|
+
else
|
144
|
+
object
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
def _deep_transform_keys_in_object!(object, &block)
|
149
|
+
case object
|
150
|
+
when Hash
|
151
|
+
object.keys.each do |key|
|
152
|
+
value = object.delete(key)
|
153
|
+
object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
|
154
|
+
end
|
155
|
+
object
|
156
|
+
when Array
|
157
|
+
object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
|
158
|
+
else
|
159
|
+
object
|
160
|
+
end
|
161
|
+
end
|
162
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
class Hash
|
2
|
+
# Merges the caller into +other_hash+. For example,
|
3
|
+
#
|
4
|
+
# options = options.reverse_merge(size: 25, velocity: 10)
|
5
|
+
#
|
6
|
+
# is equivalent to
|
7
|
+
#
|
8
|
+
# options = { size: 25, velocity: 10 }.merge(options)
|
9
|
+
#
|
10
|
+
# This is particularly useful for initializing an options hash
|
11
|
+
# with default values.
|
12
|
+
def reverse_merge(other_hash)
|
13
|
+
other_hash.merge(self)
|
14
|
+
end
|
15
|
+
|
16
|
+
# Destructive +reverse_merge+.
|
17
|
+
def reverse_merge!(other_hash)
|
18
|
+
# right wins if there is no left
|
19
|
+
merge!( other_hash ){|key,left,right| left }
|
20
|
+
end
|
21
|
+
alias_method :reverse_update, :reverse_merge!
|
22
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
class Hash
|
2
|
+
# Slice a hash to include only the given keys. This is useful for
|
3
|
+
# limiting an options hash to valid keys before passing to a method:
|
4
|
+
#
|
5
|
+
# def search(criteria = {})
|
6
|
+
# criteria.assert_valid_keys(:mass, :velocity, :time)
|
7
|
+
# end
|
8
|
+
#
|
9
|
+
# search(options.slice(:mass, :velocity, :time))
|
10
|
+
#
|
11
|
+
# If you have an array of keys you want to limit to, you should splat them:
|
12
|
+
#
|
13
|
+
# valid_keys = [:mass, :velocity, :time]
|
14
|
+
# search(options.slice(*valid_keys))
|
15
|
+
def slice(*keys)
|
16
|
+
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
17
|
+
keys.each_with_object(self.class.new) { |k, hash| hash[k] = self[k] if has_key?(k) }
|
18
|
+
end
|
19
|
+
|
20
|
+
# Replaces the hash with only the given keys.
|
21
|
+
# Returns a hash containing the removed key/value pairs.
|
22
|
+
#
|
23
|
+
# { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
|
24
|
+
# # => {:c=>3, :d=>4}
|
25
|
+
def slice!(*keys)
|
26
|
+
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
|
27
|
+
omit = slice(*self.keys - keys)
|
28
|
+
hash = slice(*keys)
|
29
|
+
hash.default = default
|
30
|
+
hash.default_proc = default_proc if default_proc
|
31
|
+
replace(hash)
|
32
|
+
omit
|
33
|
+
end
|
34
|
+
|
35
|
+
# Removes and returns the key/value pairs matching the given keys.
|
36
|
+
#
|
37
|
+
# { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => {:a=>1, :b=>2}
|
38
|
+
# { a: 1, b: 2 }.extract!(:a, :x) # => {:a=>1}
|
39
|
+
def extract!(*keys)
|
40
|
+
keys.each_with_object(self.class.new) { |key, result| result[key] = delete(key) if has_key?(key) }
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,272 @@
|
|
1
|
+
require 'active_support/core_ext/hash/keys'
|
2
|
+
|
3
|
+
module ActiveSupport
|
4
|
+
# Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
|
5
|
+
# to be the same.
|
6
|
+
#
|
7
|
+
# rgb = ActiveSupport::HashWithIndifferentAccess.new
|
8
|
+
#
|
9
|
+
# rgb[:black] = '#000000'
|
10
|
+
# rgb[:black] # => '#000000'
|
11
|
+
# rgb['black'] # => '#000000'
|
12
|
+
#
|
13
|
+
# rgb['white'] = '#FFFFFF'
|
14
|
+
# rgb[:white] # => '#FFFFFF'
|
15
|
+
# rgb['white'] # => '#FFFFFF'
|
16
|
+
#
|
17
|
+
# Internally symbols are mapped to strings when used as keys in the entire
|
18
|
+
# writing interface (calling <tt>[]=</tt>, <tt>merge</tt>, etc). This
|
19
|
+
# mapping belongs to the public interface. For example, given:
|
20
|
+
#
|
21
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
|
22
|
+
#
|
23
|
+
# You are guaranteed that the key is returned as a string:
|
24
|
+
#
|
25
|
+
# hash.keys # => ["a"]
|
26
|
+
#
|
27
|
+
# Technically other types of keys are accepted:
|
28
|
+
#
|
29
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new(a: 1)
|
30
|
+
# hash[0] = 0
|
31
|
+
# hash # => {"a"=>1, 0=>0}
|
32
|
+
#
|
33
|
+
# but this class is intended for use cases where strings or symbols are the
|
34
|
+
# expected keys and it is convenient to understand both as the same. For
|
35
|
+
# example the +params+ hash in Ruby on Rails.
|
36
|
+
#
|
37
|
+
# Note that core extensions define <tt>Hash#with_indifferent_access</tt>:
|
38
|
+
#
|
39
|
+
# rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access
|
40
|
+
#
|
41
|
+
# which may be handy.
|
42
|
+
class HashWithIndifferentAccess < Hash
|
43
|
+
# Returns +true+ so that <tt>Array#extract_options!</tt> finds members of
|
44
|
+
# this class.
|
45
|
+
def extractable_options?
|
46
|
+
true
|
47
|
+
end
|
48
|
+
|
49
|
+
def with_indifferent_access
|
50
|
+
dup
|
51
|
+
end
|
52
|
+
|
53
|
+
def nested_under_indifferent_access
|
54
|
+
self
|
55
|
+
end
|
56
|
+
|
57
|
+
def initialize(constructor = {})
|
58
|
+
if constructor.is_a?(Hash)
|
59
|
+
super()
|
60
|
+
update(constructor)
|
61
|
+
else
|
62
|
+
super(constructor)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
def default(key = nil)
|
67
|
+
if key.is_a?(Symbol) && include?(key = key.to_s)
|
68
|
+
self[key]
|
69
|
+
else
|
70
|
+
super
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.new_from_hash_copying_default(hash)
|
75
|
+
hash = hash.to_hash
|
76
|
+
new(hash).tap do |new_hash|
|
77
|
+
new_hash.default = hash.default
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
def self.[](*args)
|
82
|
+
new.merge!(Hash[*args])
|
83
|
+
end
|
84
|
+
|
85
|
+
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
|
86
|
+
alias_method :regular_update, :update unless method_defined?(:regular_update)
|
87
|
+
|
88
|
+
# Assigns a new value to the hash:
|
89
|
+
#
|
90
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new
|
91
|
+
# hash[:key] = 'value'
|
92
|
+
#
|
93
|
+
# This value can be later fetched using either +:key+ or +'key'+.
|
94
|
+
def []=(key, value)
|
95
|
+
regular_writer(convert_key(key), convert_value(value, for: :assignment))
|
96
|
+
end
|
97
|
+
|
98
|
+
alias_method :store, :[]=
|
99
|
+
|
100
|
+
# Updates the receiver in-place, merging in the hash passed as argument:
|
101
|
+
#
|
102
|
+
# hash_1 = ActiveSupport::HashWithIndifferentAccess.new
|
103
|
+
# hash_1[:key] = 'value'
|
104
|
+
#
|
105
|
+
# hash_2 = ActiveSupport::HashWithIndifferentAccess.new
|
106
|
+
# hash_2[:key] = 'New Value!'
|
107
|
+
#
|
108
|
+
# hash_1.update(hash_2) # => {"key"=>"New Value!"}
|
109
|
+
#
|
110
|
+
# The argument can be either an
|
111
|
+
# <tt>ActiveSupport::HashWithIndifferentAccess</tt> or a regular +Hash+.
|
112
|
+
# In either case the merge respects the semantics of indifferent access.
|
113
|
+
#
|
114
|
+
# If the argument is a regular hash with keys +:key+ and +"key"+ only one
|
115
|
+
# of the values end up in the receiver, but which one is unspecified.
|
116
|
+
#
|
117
|
+
# When given a block, the value for duplicated keys will be determined
|
118
|
+
# by the result of invoking the block with the duplicated key, the value
|
119
|
+
# in the receiver, and the value in +other_hash+. The rules for duplicated
|
120
|
+
# keys follow the semantics of indifferent access:
|
121
|
+
#
|
122
|
+
# hash_1[:key] = 10
|
123
|
+
# hash_2['key'] = 12
|
124
|
+
# hash_1.update(hash_2) { |key, old, new| old + new } # => {"key"=>22}
|
125
|
+
def update(other_hash)
|
126
|
+
if other_hash.is_a? HashWithIndifferentAccess
|
127
|
+
super(other_hash)
|
128
|
+
else
|
129
|
+
other_hash.to_hash.each_pair do |key, value|
|
130
|
+
if block_given? && key?(key)
|
131
|
+
value = yield(convert_key(key), self[key], value)
|
132
|
+
end
|
133
|
+
regular_writer(convert_key(key), convert_value(value))
|
134
|
+
end
|
135
|
+
self
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
alias_method :merge!, :update
|
140
|
+
|
141
|
+
# Checks the hash for a key matching the argument passed in:
|
142
|
+
#
|
143
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new
|
144
|
+
# hash['key'] = 'value'
|
145
|
+
# hash.key?(:key) # => true
|
146
|
+
# hash.key?('key') # => true
|
147
|
+
def key?(key)
|
148
|
+
super(convert_key(key))
|
149
|
+
end
|
150
|
+
|
151
|
+
alias_method :include?, :key?
|
152
|
+
alias_method :has_key?, :key?
|
153
|
+
alias_method :member?, :key?
|
154
|
+
|
155
|
+
# Same as <tt>Hash#fetch</tt> where the key passed as argument can be
|
156
|
+
# either a string or a symbol:
|
157
|
+
#
|
158
|
+
# counters = ActiveSupport::HashWithIndifferentAccess.new
|
159
|
+
# counters[:foo] = 1
|
160
|
+
#
|
161
|
+
# counters.fetch('foo') # => 1
|
162
|
+
# counters.fetch(:bar, 0) # => 0
|
163
|
+
# counters.fetch(:bar) { |key| 0 } # => 0
|
164
|
+
# counters.fetch(:zoo) # => KeyError: key not found: "zoo"
|
165
|
+
def fetch(key, *extras)
|
166
|
+
super(convert_key(key), *extras)
|
167
|
+
end
|
168
|
+
|
169
|
+
# Returns an array of the values at the specified indices:
|
170
|
+
#
|
171
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new
|
172
|
+
# hash[:a] = 'x'
|
173
|
+
# hash[:b] = 'y'
|
174
|
+
# hash.values_at('a', 'b') # => ["x", "y"]
|
175
|
+
def values_at(*indices)
|
176
|
+
indices.collect { |key| self[convert_key(key)] }
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns an exact copy of the hash.
|
180
|
+
def dup
|
181
|
+
self.class.new(self).tap do |new_hash|
|
182
|
+
new_hash.default = default
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
# This method has the same semantics of +update+, except it does not
|
187
|
+
# modify the receiver but rather returns a new hash with indifferent
|
188
|
+
# access with the result of the merge.
|
189
|
+
def merge(hash, &block)
|
190
|
+
self.dup.update(hash, &block)
|
191
|
+
end
|
192
|
+
|
193
|
+
# Like +merge+ but the other way around: Merges the receiver into the
|
194
|
+
# argument and returns a new hash with indifferent access as result:
|
195
|
+
#
|
196
|
+
# hash = ActiveSupport::HashWithIndifferentAccess.new
|
197
|
+
# hash['a'] = nil
|
198
|
+
# hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
|
199
|
+
def reverse_merge(other_hash)
|
200
|
+
super(self.class.new_from_hash_copying_default(other_hash))
|
201
|
+
end
|
202
|
+
|
203
|
+
# Same semantics as +reverse_merge+ but modifies the receiver in-place.
|
204
|
+
def reverse_merge!(other_hash)
|
205
|
+
replace(reverse_merge( other_hash ))
|
206
|
+
end
|
207
|
+
|
208
|
+
# Replaces the contents of this hash with other_hash.
|
209
|
+
#
|
210
|
+
# h = { "a" => 100, "b" => 200 }
|
211
|
+
# h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
|
212
|
+
def replace(other_hash)
|
213
|
+
super(self.class.new_from_hash_copying_default(other_hash))
|
214
|
+
end
|
215
|
+
|
216
|
+
# Removes the specified key from the hash.
|
217
|
+
def delete(key)
|
218
|
+
super(convert_key(key))
|
219
|
+
end
|
220
|
+
|
221
|
+
def stringify_keys!; self end
|
222
|
+
def deep_stringify_keys!; self end
|
223
|
+
def stringify_keys; dup end
|
224
|
+
def deep_stringify_keys; dup end
|
225
|
+
undef :symbolize_keys!
|
226
|
+
undef :deep_symbolize_keys!
|
227
|
+
def symbolize_keys; to_hash.symbolize_keys! end
|
228
|
+
def deep_symbolize_keys; to_hash.deep_symbolize_keys! end
|
229
|
+
def to_options!; self end
|
230
|
+
|
231
|
+
def select(*args, &block)
|
232
|
+
dup.tap { |hash| hash.select!(*args, &block) }
|
233
|
+
end
|
234
|
+
|
235
|
+
def reject(*args, &block)
|
236
|
+
dup.tap { |hash| hash.reject!(*args, &block) }
|
237
|
+
end
|
238
|
+
|
239
|
+
# Convert to a regular hash with string keys.
|
240
|
+
def to_hash
|
241
|
+
_new_hash= {}
|
242
|
+
each do |key, value|
|
243
|
+
_new_hash[convert_key(key)] = convert_value(value, for: :to_hash)
|
244
|
+
end
|
245
|
+
Hash.new(default).merge!(_new_hash)
|
246
|
+
end
|
247
|
+
|
248
|
+
protected
|
249
|
+
def convert_key(key)
|
250
|
+
key.kind_of?(Symbol) ? key.to_s : key
|
251
|
+
end
|
252
|
+
|
253
|
+
def convert_value(value, options = {})
|
254
|
+
if value.is_a? Hash
|
255
|
+
if options[:for] == :to_hash
|
256
|
+
value.to_hash
|
257
|
+
else
|
258
|
+
value.nested_under_indifferent_access
|
259
|
+
end
|
260
|
+
elsif value.is_a?(Array)
|
261
|
+
unless options[:for] == :assignment
|
262
|
+
value = value.dup
|
263
|
+
end
|
264
|
+
value.map! { |e| convert_value(e, options) }
|
265
|
+
else
|
266
|
+
value
|
267
|
+
end
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
272
|
+
HashWithIndifferentAccess = ActiveSupport::HashWithIndifferentAccess
|