chamber 2.13.0 → 3.0.0rc1
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data.tar.gz.sig +0 -0
- data/README.md +101 -26
- data/lib/chamber.rb +77 -16
- data/lib/chamber/adapters/cloud/circle_ci.rb +16 -13
- data/lib/chamber/adapters/cloud/heroku.rb +40 -13
- data/lib/chamber/binary/circle_ci.rb +28 -14
- data/lib/chamber/binary/heroku.rb +34 -14
- data/lib/chamber/binary/runner.rb +40 -29
- data/lib/chamber/binary/travis.rb +10 -4
- data/lib/chamber/commands/base.rb +10 -16
- data/lib/chamber/commands/cloud/base.rb +3 -3
- data/lib/chamber/commands/cloud/pull.rb +2 -2
- data/lib/chamber/commands/cloud/push.rb +7 -7
- data/lib/chamber/commands/comparable.rb +2 -2
- data/lib/chamber/commands/compare.rb +6 -9
- data/lib/chamber/commands/initialize.rb +26 -22
- data/lib/chamber/commands/securable.rb +12 -9
- data/lib/chamber/commands/secure.rb +2 -2
- data/lib/chamber/commands/show.rb +8 -8
- data/lib/chamber/commands/sign.rb +2 -2
- data/lib/chamber/commands/verify.rb +2 -2
- data/lib/chamber/configuration.rb +6 -3
- data/lib/chamber/context_resolver.rb +7 -7
- data/lib/chamber/encryption_methods/ssl.rb +12 -12
- data/lib/chamber/file.rb +20 -15
- data/lib/chamber/file_set.rb +18 -8
- data/lib/chamber/files/signature.rb +16 -14
- data/lib/chamber/filters/decryption_filter.rb +19 -15
- data/lib/chamber/filters/encryption_filter.rb +14 -11
- data/lib/chamber/filters/environment_filter.rb +21 -20
- data/lib/chamber/filters/failed_decryption_filter.rb +9 -6
- data/lib/chamber/filters/insecure_filter.rb +4 -5
- data/lib/chamber/filters/namespace_filter.rb +13 -9
- data/lib/chamber/filters/secure_filter.rb +9 -7
- data/lib/chamber/filters/translate_secure_keys_filter.rb +9 -7
- data/lib/chamber/instance.rb +48 -31
- data/lib/chamber/key_pair.rb +7 -7
- data/lib/chamber/keys/base.rb +13 -13
- data/lib/chamber/keys/decryption.rb +3 -3
- data/lib/chamber/keys/encryption.rb +3 -3
- data/lib/chamber/namespace_set.rb +2 -4
- data/lib/chamber/refinements/array.rb +20 -0
- data/lib/chamber/refinements/deep_dup.rb +58 -0
- data/lib/chamber/refinements/enumerable.rb +36 -0
- data/lib/chamber/refinements/hash.rb +51 -0
- data/lib/chamber/settings.rb +81 -66
- data/lib/chamber/types/secured.rb +21 -23
- data/lib/chamber/version.rb +1 -1
- data/templates/settings.yml +2 -0
- metadata +44 -51
- metadata.gz.sig +0 -0
- data/lib/chamber/core_ext/hash.rb +0 -15
data/lib/chamber/key_pair.rb
CHANGED
@@ -9,10 +9,10 @@ class KeyPair
|
|
9
9
|
:namespace,
|
10
10
|
:passphrase
|
11
11
|
|
12
|
-
def initialize(
|
13
|
-
self.namespace =
|
14
|
-
self.passphrase =
|
15
|
-
self.key_file_path = Pathname.new(
|
12
|
+
def initialize(key_file_path:, namespace: nil, passphrase: ::SecureRandom.uuid)
|
13
|
+
self.namespace = namespace
|
14
|
+
self.passphrase = passphrase
|
15
|
+
self.key_file_path = Pathname.new(key_file_path)
|
16
16
|
end
|
17
17
|
|
18
18
|
def encrypted_private_key_passphrase_filepath
|
@@ -78,9 +78,9 @@ class KeyPair
|
|
78
78
|
@base_key_filename ||= [
|
79
79
|
'.chamber',
|
80
80
|
namespace ? namespace.tr('-.', '') : nil,
|
81
|
-
]
|
82
|
-
compact
|
83
|
-
join('.')
|
81
|
+
]
|
82
|
+
.compact
|
83
|
+
.join('.')
|
84
84
|
end
|
85
85
|
end
|
86
86
|
end
|
data/lib/chamber/keys/base.rb
CHANGED
@@ -3,18 +3,18 @@
|
|
3
3
|
module Chamber
|
4
4
|
module Keys
|
5
5
|
class Base
|
6
|
-
def self.resolve(
|
7
|
-
new(
|
6
|
+
def self.resolve(**args)
|
7
|
+
new(**args).resolve
|
8
8
|
end
|
9
9
|
|
10
10
|
attr_accessor :rootpath
|
11
11
|
attr_reader :filenames,
|
12
12
|
:namespaces
|
13
13
|
|
14
|
-
def initialize(
|
15
|
-
self.rootpath = Pathname.new(
|
16
|
-
self.namespaces =
|
17
|
-
self.filenames =
|
14
|
+
def initialize(rootpath:, namespaces:, filenames: nil)
|
15
|
+
self.rootpath = Pathname.new(rootpath)
|
16
|
+
self.namespaces = namespaces
|
17
|
+
self.filenames = filenames
|
18
18
|
end
|
19
19
|
|
20
20
|
def resolve
|
@@ -41,9 +41,9 @@ class Base
|
|
41
41
|
|
42
42
|
# rubocop:disable Performance/ChainArrayAllocation
|
43
43
|
def filenames=(other)
|
44
|
-
@filenames = Array(other)
|
45
|
-
map { |o| Pathname.new(o) }
|
46
|
-
compact
|
44
|
+
@filenames = Array(other)
|
45
|
+
.map { |o| Pathname.new(o) }
|
46
|
+
.compact
|
47
47
|
end
|
48
48
|
# rubocop:enable Performance/ChainArrayAllocation
|
49
49
|
|
@@ -52,10 +52,10 @@ class Base
|
|
52
52
|
end
|
53
53
|
|
54
54
|
def namespace_from_path(path)
|
55
|
-
path
|
56
|
-
basename
|
57
|
-
to_s
|
58
|
-
match(self.class::NAMESPACE_PATTERN) { |m| m[1].upcase }
|
55
|
+
path
|
56
|
+
.basename
|
57
|
+
.to_s
|
58
|
+
.match(self.class::NAMESPACE_PATTERN) { |m| m[1].upcase }
|
59
59
|
end
|
60
60
|
|
61
61
|
def namespace_to_key_path(namespace)
|
@@ -71,10 +71,8 @@ class NamespaceSet
|
|
71
71
|
# Internal: Iterates over each namespace value and allows it to be used in
|
72
72
|
# a block.
|
73
73
|
#
|
74
|
-
def each
|
75
|
-
namespaces.each
|
76
|
-
yield namespace
|
77
|
-
end
|
74
|
+
def each(&block)
|
75
|
+
namespaces.each(&block)
|
78
76
|
end
|
79
77
|
|
80
78
|
###
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspectacular'
|
4
|
+
require 'chamber/refinements/enumerable'
|
5
|
+
|
6
|
+
module Chamber
|
7
|
+
module Refinements
|
8
|
+
module Array
|
9
|
+
refine ::Array do
|
10
|
+
def deep_transform_keys(&block)
|
11
|
+
Refinements::Enumerable.deep_transform_keys(self, &block)
|
12
|
+
end
|
13
|
+
|
14
|
+
def deep_transform_values(&block)
|
15
|
+
Refinements::Enumerable.deep_transform_values(nil, self, &block)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,58 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chamber
|
4
|
+
module Refinements
|
5
|
+
module DeepDup
|
6
|
+
refine ::Array do
|
7
|
+
unless method_defined?(:deep_dup)
|
8
|
+
def deep_dup
|
9
|
+
map do |i|
|
10
|
+
if i.respond_to?(:deep_dup)
|
11
|
+
i.deep_dup
|
12
|
+
else
|
13
|
+
begin
|
14
|
+
i.dup
|
15
|
+
rescue ::TypeError
|
16
|
+
# Hack for < Ruby 2.4 since FalseClass, TrueClass, Fixnum, etc can't be
|
17
|
+
# dupped
|
18
|
+
i
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
refine ::Object do
|
27
|
+
unless method_defined?(:deep_dup)
|
28
|
+
def deep_dup
|
29
|
+
begin
|
30
|
+
dup
|
31
|
+
rescue ::TypeError
|
32
|
+
# Hack for < Ruby 2.4 since FalseClass, TrueClass, Fixnum, etc can't be
|
33
|
+
# dupped
|
34
|
+
self
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
refine ::Hash do
|
41
|
+
unless method_defined?(:deep_dup)
|
42
|
+
def deep_dup
|
43
|
+
dup.tap do |hash|
|
44
|
+
each_pair do |key, value|
|
45
|
+
if key.frozen? && key.is_a?(::String)
|
46
|
+
hash[key] = value.deep_dup
|
47
|
+
else
|
48
|
+
hash.delete(key)
|
49
|
+
hash[key.deep_dup] = value.deep_dup
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Chamber
|
4
|
+
module Refinements
|
5
|
+
class Enumerable
|
6
|
+
def self.deep_transform_keys(object, &block)
|
7
|
+
case object
|
8
|
+
when ::Hash
|
9
|
+
object.each_with_object({}) do |(key, value), result|
|
10
|
+
result[yield(key)] = deep_transform_keys(value, &block)
|
11
|
+
end
|
12
|
+
when ::Array
|
13
|
+
object.map { |e| deep_transform_keys(e, &block) }
|
14
|
+
else
|
15
|
+
object
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.deep_transform_values(key, value, &block)
|
20
|
+
case value
|
21
|
+
when ::Hash
|
22
|
+
value.each_with_object({}) do |(k, v), memo|
|
23
|
+
memo[k] = deep_transform_values(k, v, &block)
|
24
|
+
end
|
25
|
+
when ::Array
|
26
|
+
yield(
|
27
|
+
key,
|
28
|
+
value.map { |v| deep_transform_values(nil, v, &block) }
|
29
|
+
)
|
30
|
+
else
|
31
|
+
yield(key, value)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspectacular'
|
4
|
+
require 'chamber/refinements/hash'
|
5
|
+
require 'chamber/refinements/enumerable'
|
6
|
+
|
7
|
+
module Chamber
|
8
|
+
module Refinements
|
9
|
+
module Hash
|
10
|
+
refine ::Hash do
|
11
|
+
def deep_strip!
|
12
|
+
each do |key, value|
|
13
|
+
if value.respond_to?(:strip)
|
14
|
+
self[key] = value.strip
|
15
|
+
elsif value.respond_to?(:deep_strip!)
|
16
|
+
self[key] = value.deep_strip!
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def deep_transform_keys(&block)
|
22
|
+
Refinements::Enumerable.deep_transform_keys(self, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
def deep_transform_values(&block)
|
26
|
+
Refinements::Enumerable.deep_transform_values(nil, self, &block)
|
27
|
+
end
|
28
|
+
|
29
|
+
unless method_defined?(:deep_merge)
|
30
|
+
def deep_merge(other, &block)
|
31
|
+
dup.deep_merge!(other, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
unless method_defined?(:deep_merge!)
|
36
|
+
def deep_merge!(other, &block)
|
37
|
+
merge!(other) do |key, value_1, value_2|
|
38
|
+
if value_1.is_a?(::Hash) && value_2.is_a?(::Hash)
|
39
|
+
value_1.deep_merge(value_2, &block)
|
40
|
+
elsif block
|
41
|
+
yield(key, value_1, value_2)
|
42
|
+
else
|
43
|
+
value_2
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
data/lib/chamber/settings.rb
CHANGED
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'hashie/mash'
|
4
3
|
require 'chamber/namespace_set'
|
5
4
|
require 'chamber/filters/namespace_filter'
|
6
5
|
require 'chamber/filters/encryption_filter'
|
@@ -10,35 +9,52 @@ require 'chamber/filters/secure_filter'
|
|
10
9
|
require 'chamber/filters/translate_secure_keys_filter'
|
11
10
|
require 'chamber/filters/insecure_filter'
|
12
11
|
require 'chamber/filters/failed_decryption_filter'
|
12
|
+
require 'chamber/refinements/deep_dup'
|
13
|
+
require 'chamber/refinements/hash'
|
13
14
|
|
14
15
|
###
|
15
16
|
# Internal: Represents the base settings storage needed for Chamber.
|
16
17
|
#
|
17
18
|
module Chamber
|
18
19
|
class Settings
|
19
|
-
|
20
|
-
|
20
|
+
using ::Chamber::Refinements::Hash
|
21
|
+
using ::Chamber::Refinements::DeepDup
|
22
|
+
|
23
|
+
attr_accessor :decryption_keys,
|
21
24
|
:encryption_keys,
|
22
|
-
:
|
25
|
+
:post_filters,
|
26
|
+
:pre_filters,
|
27
|
+
:secure_key_prefix
|
23
28
|
attr_reader :namespaces
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
29
|
+
attr_writer :raw_data
|
30
|
+
|
31
|
+
# rubocop:disable Metrics/ParameterLists
|
32
|
+
def initialize(
|
33
|
+
decryption_keys: {},
|
34
|
+
encryption_keys: {},
|
35
|
+
namespaces: [],
|
36
|
+
pre_filters: [
|
37
|
+
Filters::NamespaceFilter,
|
38
|
+
],
|
39
|
+
post_filters: [
|
40
|
+
Filters::DecryptionFilter,
|
41
|
+
Filters::EnvironmentFilter,
|
42
|
+
Filters::FailedDecryptionFilter,
|
43
|
+
Filters::TranslateSecureKeysFilter,
|
44
|
+
],
|
45
|
+
secure_key_prefix: '_secure_',
|
46
|
+
settings: {},
|
47
|
+
**_args
|
48
|
+
)
|
49
|
+
self.decryption_keys = decryption_keys
|
50
|
+
self.encryption_keys = encryption_keys
|
51
|
+
self.namespaces = namespaces
|
52
|
+
self.post_filters = post_filters
|
53
|
+
self.pre_filters = pre_filters
|
54
|
+
self.raw_data = settings.deep_dup
|
55
|
+
self.secure_key_prefix = secure_key_prefix
|
40
56
|
end
|
41
|
-
# rubocop:enable Metrics/
|
57
|
+
# rubocop:enable Metrics/ParameterLists
|
42
58
|
|
43
59
|
###
|
44
60
|
# Internal: Converts a Settings object into a hash that is compatible as an
|
@@ -79,15 +95,11 @@ class Settings
|
|
79
95
|
# } ).to_s
|
80
96
|
# # => 'MY_KEY="my value" MY_OTHER_KEY="my other value"'
|
81
97
|
#
|
82
|
-
def to_s(
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
concatenated_name_hash = to_concatenated_name_hash(hierarchical_separator)
|
89
|
-
|
90
|
-
pairs = concatenated_name_hash.to_a.map do |key, value|
|
98
|
+
def to_s(hierarchical_separator: '_',
|
99
|
+
pair_separator: ' ',
|
100
|
+
value_surrounder: '"',
|
101
|
+
name_value_separator: '=')
|
102
|
+
pairs = to_concatenated_name_hash(hierarchical_separator).to_a.map do |key, value|
|
91
103
|
"#{key.upcase}#{name_value_separator}#{value_surrounder}#{value}#{value_surrounder}"
|
92
104
|
end
|
93
105
|
|
@@ -102,7 +114,7 @@ class Settings
|
|
102
114
|
# Returns a Hash
|
103
115
|
#
|
104
116
|
def to_hash
|
105
|
-
data.
|
117
|
+
data.dup
|
106
118
|
end
|
107
119
|
|
108
120
|
###
|
@@ -135,11 +147,11 @@ class Settings
|
|
135
147
|
flattened_name_hash = {}
|
136
148
|
|
137
149
|
hash.each_pair do |key, value|
|
138
|
-
flattened_name_components = parent_keys.
|
150
|
+
flattened_name_components = parent_keys.deep_dup.push(key)
|
139
151
|
|
140
152
|
if value.respond_to?(:each_pair)
|
141
|
-
flattened_name_hash
|
142
|
-
|
153
|
+
flattened_name_hash
|
154
|
+
.deep_merge!(to_flattened_name_hash(value, flattened_name_components))
|
143
155
|
else
|
144
156
|
flattened_name_hash[flattened_name_components] = value
|
145
157
|
end
|
@@ -182,20 +194,21 @@ class Settings
|
|
182
194
|
# Returns a new Settings object
|
183
195
|
#
|
184
196
|
def merge(other)
|
185
|
-
other_settings =
|
197
|
+
other_settings = case other
|
198
|
+
when Settings
|
186
199
|
other
|
187
|
-
|
200
|
+
when ::Hash
|
188
201
|
Settings.new(settings: other)
|
189
202
|
end
|
190
203
|
|
191
|
-
# rubocop:disable
|
204
|
+
# rubocop:disable Layout/LineLength
|
192
205
|
Settings.new(
|
193
206
|
encryption_keys: encryption_keys.any? ? encryption_keys : other_settings.encryption_keys,
|
194
207
|
decryption_keys: decryption_keys.any? ? decryption_keys : other_settings.decryption_keys,
|
195
208
|
namespaces: (namespaces + other_settings.namespaces),
|
196
|
-
settings: raw_data.
|
209
|
+
settings: raw_data.deep_merge(other_settings.raw_data),
|
197
210
|
)
|
198
|
-
# rubocop:enable
|
211
|
+
# rubocop:enable Layout/LineLength
|
199
212
|
end
|
200
213
|
|
201
214
|
###
|
@@ -219,15 +232,36 @@ class Settings
|
|
219
232
|
namespaces == other.namespaces
|
220
233
|
end
|
221
234
|
|
235
|
+
def [](key)
|
236
|
+
warn "WARNING: Bracket access will require strings instead of symbols in Chamber 3.0. You attempted to access the '#{key}' setting. See https://github.com/thekompanee/chamber/wiki/Upgrading-To-Chamber-3.0#removal-of-bracket-indifferent-access for full details." if key.is_a?(::Symbol) # rubocop:disable Layout/LineLength
|
237
|
+
warn "WARNING: Accessing a non-existent key ('#{key}') with brackets will fail in Chamber 3.0. See https://github.com/thekompanee/chamber/wiki/Upgrading-To-Chamber-3.0#bracket-access-now-fails-on-non-existent-keys for full details." unless data.has_key?(key) # rubocop:disable Layout/LineLength
|
238
|
+
|
239
|
+
data.fetch(key)
|
240
|
+
end
|
241
|
+
|
242
|
+
def dig!(*args)
|
243
|
+
args.inject(data) do |data_value, bracket_value|
|
244
|
+
key = bracket_value.is_a?(::Symbol) ? bracket_value.to_s : bracket_value
|
245
|
+
|
246
|
+
data_value.fetch(key)
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
def dig(*args)
|
251
|
+
dig!(*args)
|
252
|
+
rescue ::KeyError, ::IndexError # rubocop:disable Lint/ShadowedException
|
253
|
+
nil
|
254
|
+
end
|
255
|
+
|
222
256
|
def securable
|
223
|
-
Settings.new(metadata.
|
257
|
+
Settings.new(**metadata.deep_merge(
|
224
258
|
settings: raw_data,
|
225
259
|
pre_filters: [Filters::SecureFilter],
|
226
260
|
))
|
227
261
|
end
|
228
262
|
|
229
263
|
def secure
|
230
|
-
Settings.new(metadata.
|
264
|
+
Settings.new(**metadata.deep_merge(
|
231
265
|
settings: raw_data,
|
232
266
|
pre_filters: [Filters::EncryptionFilter],
|
233
267
|
post_filters: [Filters::TranslateSecureKeysFilter],
|
@@ -235,29 +269,15 @@ class Settings
|
|
235
269
|
end
|
236
270
|
|
237
271
|
def insecure
|
238
|
-
Settings.new(metadata.
|
272
|
+
Settings.new(**metadata.deep_merge(
|
239
273
|
settings: raw_data,
|
240
274
|
pre_filters: [Filters::InsecureFilter],
|
241
275
|
post_filters: [Filters::TranslateSecureKeysFilter],
|
242
276
|
))
|
243
277
|
end
|
244
278
|
|
245
|
-
def method_missing(name, *args)
|
246
|
-
return data.public_send(name, *args) if data.respond_to?(name)
|
247
|
-
|
248
|
-
super
|
249
|
-
end
|
250
|
-
|
251
|
-
def respond_to_missing?(name, include_private = false)
|
252
|
-
data.respond_to?(name, include_private)
|
253
|
-
end
|
254
|
-
|
255
279
|
protected
|
256
280
|
|
257
|
-
def raw_data=(new_raw_data)
|
258
|
-
@raw_data = Hashie::Mash.new(new_raw_data)
|
259
|
-
end
|
260
|
-
|
261
281
|
def namespaces=(raw_namespaces)
|
262
282
|
@namespaces = NamespaceSet.new(raw_namespaces)
|
263
283
|
end
|
@@ -265,21 +285,16 @@ class Settings
|
|
265
285
|
# rubocop:disable Naming/MemoizedInstanceVariableName
|
266
286
|
def raw_data
|
267
287
|
@filtered_raw_data ||= pre_filters.inject(@raw_data) do |filtered_data, filter|
|
268
|
-
filter.execute({ data: filtered_data }.
|
269
|
-
merge(metadata))
|
288
|
+
filter.execute(**{ data: filtered_data }.deep_merge(metadata))
|
270
289
|
end
|
271
290
|
end
|
272
291
|
# rubocop:enable Naming/MemoizedInstanceVariableName
|
273
292
|
|
274
293
|
def data
|
275
|
-
@data ||= post_filters
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
end
|
280
|
-
|
281
|
-
def secure_key_prefix
|
282
|
-
'_secure_'
|
294
|
+
@data ||= post_filters
|
295
|
+
.inject(raw_data) do |filtered_data, filter|
|
296
|
+
filter.execute(**{ data: filtered_data }.deep_merge(metadata))
|
297
|
+
end
|
283
298
|
end
|
284
299
|
|
285
300
|
def metadata
|