phraseapp_updater 2.0.4 → 2.0.5
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
- data/bin/phraseapp_updater +13 -7
- data/bin/synchronize_phraseapp.sh +9 -2
- data/lib/phraseapp_updater.rb +6 -4
- data/lib/phraseapp_updater/differ.rb +124 -108
- data/lib/phraseapp_updater/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 6279bf1f85138efd48f74dfad8c33bc374c484d763a13864a985b6b7008cb877
|
4
|
+
data.tar.gz: 4d18b2a5b27fed7564625b9ee19b4172a1bfabf976cb889d8f468ac57e3c9a22
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 8d31f320c4c72c182f3664f90a9d19fe07a8b5debc4d4253f092c5ab16effa25aa94ac2281a6a3a857da500042f354ad20c5852a943fd311faf5dbbd79bd8f38
|
7
|
+
data.tar.gz: 1ab2a71f775c3eb7ed9c0d8c2d2ca9ee92b5d633de1439e929a096fa9f1b3b7ee66da9e6756ae058804936f37ce1a5c075306853bfd68c2a541cca760bb7a4f5
|
data/bin/phraseapp_updater
CHANGED
@@ -7,6 +7,7 @@ require 'phraseapp_updater'
|
|
7
7
|
class PhraseAppUpdaterCLI < Thor
|
8
8
|
class_option :default_locale, type: :string, default: 'en', desc: 'PhraseApp default locale'
|
9
9
|
class_option :file_format, type: :string, default: 'json', desc: 'Filetype of localization files.'
|
10
|
+
class_option :verbose, type: :boolean, default: false, desc: 'Verbose output'
|
10
11
|
|
11
12
|
desc 'setup <locale_path>',
|
12
13
|
'Create a new PhraseApp project, initializing it with locale files at <locale_path>. the new project ID is printed to STDOUT'
|
@@ -22,7 +23,8 @@ class PhraseAppUpdaterCLI < Thor
|
|
22
23
|
options[:phraseapp_api_key],
|
23
24
|
options[:phraseapp_project_name],
|
24
25
|
options[:file_format],
|
25
|
-
options[:parent_commit]
|
26
|
+
options[:parent_commit],
|
27
|
+
verbose: options[:verbose])
|
26
28
|
|
27
29
|
updater.upload_directory(locales_path)
|
28
30
|
|
@@ -50,6 +52,7 @@ class PhraseAppUpdaterCLI < Thor
|
|
50
52
|
ENV['PREFIX'] = options[:prefix]
|
51
53
|
ENV['BRANCH'] = options.fetch(:branch) { sh('git rev-parse --abbrev-ref HEAD').chomp }
|
52
54
|
ENV['REMOTE'] = options.fetch(:remote) { sh("git config branch.#{ENV['BRANCH']}.remote").chomp }
|
55
|
+
ENV['VERBOSE'] = options[:verbose] ? 't' : 'f'
|
53
56
|
|
54
57
|
shell_script_path = File.join(__dir__, 'synchronize_phraseapp.sh')
|
55
58
|
exec(shell_script_path)
|
@@ -67,7 +70,8 @@ class PhraseAppUpdaterCLI < Thor
|
|
67
70
|
updater = PhraseAppUpdater.new(
|
68
71
|
options[:phraseapp_api_key],
|
69
72
|
options[:phraseapp_project_id],
|
70
|
-
options[:file_format]
|
73
|
+
options[:file_format],
|
74
|
+
verbose: options[:verbose])
|
71
75
|
|
72
76
|
updater.download_to_directory(target_path)
|
73
77
|
parent_commit = updater.read_parent_commit
|
@@ -94,7 +98,8 @@ class PhraseAppUpdaterCLI < Thor
|
|
94
98
|
updater = PhraseAppUpdater.new(
|
95
99
|
options[:phraseapp_api_key],
|
96
100
|
options[:phraseapp_project_id],
|
97
|
-
options[:file_format]
|
101
|
+
options[:file_format],
|
102
|
+
verbose: options[:verbose])
|
98
103
|
|
99
104
|
updater.upload_directory(source_path)
|
100
105
|
updater.update_parent_commit(options[:parent_commit])
|
@@ -110,7 +115,8 @@ class PhraseAppUpdaterCLI < Thor
|
|
110
115
|
updater = PhraseAppUpdater.new(
|
111
116
|
options[:phraseapp_api_key],
|
112
117
|
options[:phraseapp_project_id],
|
113
|
-
options[:file_format]
|
118
|
+
options[:file_format],
|
119
|
+
verbose: options[:verbose])
|
114
120
|
|
115
121
|
updater.update_parent_commit(options[:parent_commit])
|
116
122
|
end
|
@@ -131,7 +137,7 @@ class PhraseAppUpdaterCLI < Thor
|
|
131
137
|
validate_readable_path!('path2', path2)
|
132
138
|
|
133
139
|
handle_errors do
|
134
|
-
updater = PhraseAppUpdater.new(nil, nil, options[:file_format])
|
140
|
+
updater = PhraseAppUpdater.new(nil, nil, options[:file_format], verbose: options[:verbose])
|
135
141
|
diffs = updater.diff_directories(path1, path2)
|
136
142
|
if diffs.empty?
|
137
143
|
exit(0)
|
@@ -162,7 +168,7 @@ class PhraseAppUpdaterCLI < Thor
|
|
162
168
|
validate_writable_path!('to', result_path)
|
163
169
|
|
164
170
|
handle_errors do
|
165
|
-
updater = PhraseAppUpdater.new(nil, nil, options[:file_format])
|
171
|
+
updater = PhraseAppUpdater.new(nil, nil, options[:file_format], verbose: options[:verbose])
|
166
172
|
updater.merge_directories(our_path, their_path, ancestor_path, result_path)
|
167
173
|
end
|
168
174
|
end
|
@@ -192,7 +198,7 @@ class PhraseAppUpdaterCLI < Thor
|
|
192
198
|
# pass `nil` to `merge_files`.
|
193
199
|
ancestor = nil if File.zero?(ancestor)
|
194
200
|
|
195
|
-
updater = PhraseAppUpdater.new(nil, nil, file_format)
|
201
|
+
updater = PhraseAppUpdater.new(nil, nil, file_format, verbose: options[:verbose])
|
196
202
|
updater.merge_files(ours, theirs, ancestor, to)
|
197
203
|
end
|
198
204
|
|
@@ -13,7 +13,7 @@ fi
|
|
13
13
|
|
14
14
|
# Configuration is via environment, and expected to be provided from a Ruby
|
15
15
|
# driver.
|
16
|
-
for variable in PHRASEAPP_API_KEY PHRASEAPP_PROJECT_ID BRANCH REMOTE PREFIX FILE_FORMAT NO_COMMIT; do
|
16
|
+
for variable in PHRASEAPP_API_KEY PHRASEAPP_PROJECT_ID BRANCH REMOTE PREFIX FILE_FORMAT NO_COMMIT VERBOSE; do
|
17
17
|
if [ -z "${!variable}" ]; then
|
18
18
|
echo "Error: must specify $variable" >&2
|
19
19
|
exit 1
|
@@ -30,6 +30,7 @@ current_phraseapp_path=$(make_temporary_directory)
|
|
30
30
|
common_ancestor=$(phraseapp_updater download "${current_phraseapp_path}" \
|
31
31
|
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
32
32
|
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}" \
|
33
|
+
--verbose="${VERBOSE}" \
|
33
34
|
--file_format="${FILE_FORMAT}")
|
34
35
|
|
35
36
|
# If common_ancestor is not available or reachable from BRANCH, we've been
|
@@ -74,6 +75,7 @@ if [ "${phraseapp_changed}" = 't' ] && [ "${branch_changed}" = 't' ]; then
|
|
74
75
|
merge_resolution_path=$(make_temporary_directory)
|
75
76
|
phraseapp_updater merge "${common_ancestor_path}" "${current_branch_path}" "${current_phraseapp_path}" \
|
76
77
|
--to "${merge_resolution_path}" \
|
78
|
+
--verbose="${VERBOSE}" \
|
77
79
|
--file_format="${FILE_FORMAT}"
|
78
80
|
|
79
81
|
if [ "$NO_COMMIT" != 't' ]; then
|
@@ -92,6 +94,7 @@ if [ "${phraseapp_changed}" = 't' ] && [ "${branch_changed}" = 't' ]; then
|
|
92
94
|
-p "${current_branch}" \
|
93
95
|
-p "${phraseapp_commit}" \
|
94
96
|
-m "Merged locale changes from PhraseApp" \
|
97
|
+
-m "Since common ancestor ${common_ancestor}" \
|
95
98
|
-m "X-PhraseApp-Merge: ${phraseapp_commit}")
|
96
99
|
|
97
100
|
# Push to BRANCH
|
@@ -108,6 +111,7 @@ if [ "${phraseapp_changed}" = 't' ] && [ "${branch_changed}" = 't' ]; then
|
|
108
111
|
--parent_commit="${new_parent_commit}" \
|
109
112
|
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
110
113
|
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}" \
|
114
|
+
--verbose="${VERBOSE}" \
|
111
115
|
--file_format="${FILE_FORMAT}"
|
112
116
|
|
113
117
|
elif [ "${branch_changed}" = 't' ]; then
|
@@ -118,6 +122,7 @@ elif [ "${branch_changed}" = 't' ]; then
|
|
118
122
|
--parent_commit="${current_branch}" \
|
119
123
|
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
120
124
|
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}" \
|
125
|
+
--verbose="${VERBOSE}" \
|
121
126
|
--file_format="${FILE_FORMAT}"
|
122
127
|
|
123
128
|
elif [ "${phraseapp_changed}" = 't' ]; then
|
@@ -127,7 +132,8 @@ elif [ "${phraseapp_changed}" = 't' ]; then
|
|
127
132
|
updated_branch_tree=$(replace_nested_tree "${current_branch}^{tree}" "${PREFIX}" "${current_phraseapp_tree}")
|
128
133
|
update_commit=$(git commit-tree "${updated_branch_tree}" \
|
129
134
|
-p "${current_branch}" \
|
130
|
-
-m "Incorporate locale changes from PhraseApp"
|
135
|
+
-m "Incorporate locale changes from PhraseApp" \
|
136
|
+
-m "Since common ancestor ${common_ancestor}")
|
131
137
|
|
132
138
|
git push "${REMOTE}" "${update_commit}:refs/heads/${BRANCH}"
|
133
139
|
|
@@ -135,6 +141,7 @@ elif [ "${phraseapp_changed}" = 't' ]; then
|
|
135
141
|
phraseapp_updater update_parent_commit \
|
136
142
|
--parent_commit="${update_commit}" \
|
137
143
|
--phraseapp_api_key="${PHRASEAPP_API_KEY}" \
|
144
|
+
--verbose="${VERBOSE}" \
|
138
145
|
--phraseapp_project_id="${PHRASEAPP_PROJECT_ID}"
|
139
146
|
else
|
140
147
|
echo "Only PhraseApp changed: not committing to $BRANCH branch" >&2
|
data/lib/phraseapp_updater.rb
CHANGED
@@ -10,16 +10,17 @@ require 'phraseapp_updater/yml_config_loader'
|
|
10
10
|
class PhraseAppUpdater
|
11
11
|
using IndexBy
|
12
12
|
|
13
|
-
def self.for_new_project(phraseapp_api_key, phraseapp_project_name, file_format, parent_commit)
|
13
|
+
def self.for_new_project(phraseapp_api_key, phraseapp_project_name, file_format, parent_commit, verbose: false)
|
14
14
|
api = PhraseAppAPI.new(phraseapp_api_key, nil, LocaleFile.class_for_file_format(file_format))
|
15
15
|
project_id = api.create_project(phraseapp_project_name, parent_commit)
|
16
|
-
return self.new(phraseapp_api_key, project_id, file_format), project_id
|
16
|
+
return self.new(phraseapp_api_key, project_id, file_format, verbose: verbose), project_id
|
17
17
|
end
|
18
18
|
|
19
|
-
def initialize(phraseapp_api_key, phraseapp_project_id, file_format, default_locale: 'en')
|
19
|
+
def initialize(phraseapp_api_key, phraseapp_project_id, file_format, default_locale: 'en', verbose: false)
|
20
20
|
@locale_file_class = LocaleFile.class_for_file_format(file_format)
|
21
21
|
@default_locale = default_locale
|
22
22
|
@phraseapp_api = PhraseAppAPI.new(phraseapp_api_key, phraseapp_project_id, @locale_file_class)
|
23
|
+
@verbose = verbose
|
23
24
|
end
|
24
25
|
|
25
26
|
def diff_directories(our_path, their_path)
|
@@ -84,6 +85,7 @@ class PhraseAppUpdater
|
|
84
85
|
locale_names = Set.new.merge(ours.keys).merge(theirs.keys)
|
85
86
|
|
86
87
|
locale_names.map do |locale_name|
|
88
|
+
STDERR.puts "Merging #{locale_name}" if @verbose
|
87
89
|
our_file = ours[locale_name]
|
88
90
|
their_file = theirs[locale_name]
|
89
91
|
ancestor_file = ancestors[locale_name]
|
@@ -118,7 +120,7 @@ class PhraseAppUpdater
|
|
118
120
|
else
|
119
121
|
ancestor_file ||= empty_locale_file(our_file.locale_name)
|
120
122
|
|
121
|
-
resolved_content = Differ.resolve!(
|
123
|
+
resolved_content = Differ.new(verbose: @verbose).resolve!(
|
122
124
|
original: ancestor_file.parsed_content,
|
123
125
|
primary: our_file.parsed_content,
|
124
126
|
secondary: their_file.parsed_content)
|
@@ -1,144 +1,160 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'set'
|
2
4
|
require 'hashdiff'
|
3
5
|
require 'deep_merge'
|
4
6
|
|
5
7
|
class PhraseAppUpdater
|
6
8
|
class Differ
|
7
|
-
SEPARATOR =
|
9
|
+
SEPARATOR = '~~~'
|
8
10
|
using IndexBy
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
primary = primary.index_by { |op, path, from, to| path }
|
14
|
-
secondary = secondary.index_by { |op, path, from, to| path }
|
15
|
-
|
16
|
-
# As well as explicit conflicts, we want to make sure that deletions or
|
17
|
-
# incompatible type changes to a `primary` key prevent addition of child
|
18
|
-
# keys in `secondary`. Because input hashes are flattened, it's never
|
19
|
-
# possible for a given path and its prefix to be in the same input.
|
20
|
-
# For example, in:
|
21
|
-
#
|
22
|
-
# primary = [["+", "a", 1]]
|
23
|
-
# secondary = [["+", "a.b", 2]]
|
24
|
-
#
|
25
|
-
# the secondary change is impossible to perform on top of the primary, and
|
26
|
-
# must be blocked.
|
27
|
-
#
|
28
|
-
# This applies in reverse: prefixes of paths in `p` need to be available
|
29
|
-
# as hashes, so must not appear as terminals in `s`:
|
30
|
-
#
|
31
|
-
# primary = [["+", "a.b", 2]]
|
32
|
-
# secondary = [["+", "a", 1]]
|
33
|
-
primary_prefixes = primary.keys.flat_map { |p| path_prefixes(p) }.to_set
|
34
|
-
|
35
|
-
# Remove conflicting entries from secondary, recording incompatible
|
36
|
-
# changes.
|
37
|
-
path_conflicts = []
|
38
|
-
secondary.delete_if do |path, diff|
|
39
|
-
if primary_prefixes.include?(path) || primary.keys.any? { |pk| path.start_with?(pk) }
|
40
|
-
path_conflicts << path unless primary.has_key?(path) && diff == primary[path]
|
41
|
-
true
|
42
|
-
else
|
43
|
-
false
|
44
|
-
end
|
45
|
-
end
|
46
|
-
|
47
|
-
# For all path conflicts matching secondary_deleted_prefixes, additionally
|
48
|
-
# remove other changes with the same prefix.
|
49
|
-
prefix_conflicts = secondary_deleted_prefixes.select do |prefix|
|
50
|
-
path_conflicts.any? { |path| path.start_with?(prefix) }
|
51
|
-
end
|
12
|
+
def initialize(verbose: false)
|
13
|
+
@verbose = verbose
|
14
|
+
end
|
52
15
|
|
53
|
-
|
54
|
-
|
16
|
+
# Resolution strategy is that primary always wins in the event of a conflict
|
17
|
+
def resolve_diffs(primary:, secondary:, secondary_deleted_prefixes:)
|
18
|
+
primary = primary.index_by { |op, path, from, to| path }
|
19
|
+
secondary = secondary.index_by { |op, path, from, to| path }
|
20
|
+
|
21
|
+
# As well as explicit conflicts, we want to make sure that deletions or
|
22
|
+
# incompatible type changes to a `primary` key prevent addition of child
|
23
|
+
# keys in `secondary`. Because input hashes are flattened, it's never
|
24
|
+
# possible for a given path and its prefix to be in the same input.
|
25
|
+
# For example, in:
|
26
|
+
#
|
27
|
+
# primary = [["+", "a", 1]]
|
28
|
+
# secondary = [["+", "a.b", 2]]
|
29
|
+
#
|
30
|
+
# the secondary change is impossible to perform on top of the primary, and
|
31
|
+
# must be blocked.
|
32
|
+
#
|
33
|
+
# This applies in reverse: prefixes of paths in `p` need to be available
|
34
|
+
# as hashes, so must not appear as terminals in `s`:
|
35
|
+
#
|
36
|
+
# primary = [["+", "a.b", 2]]
|
37
|
+
# secondary = [["+", "a", 1]]
|
38
|
+
primary_prefixes = primary.keys.flat_map { |p| path_prefixes(p) }.to_set
|
39
|
+
|
40
|
+
# Remove conflicting entries from secondary, recording incompatible
|
41
|
+
# changes.
|
42
|
+
path_conflicts = []
|
43
|
+
secondary.delete_if do |path, diff|
|
44
|
+
if primary_prefixes.include?(path) || primary.keys.any? { |pk| path.start_with?(pk) }
|
45
|
+
path_conflicts << path unless primary.has_key?(path) && diff == primary[path]
|
46
|
+
true
|
47
|
+
else
|
48
|
+
false
|
55
49
|
end
|
50
|
+
end
|
56
51
|
|
57
|
-
|
52
|
+
# For all path conflicts matching secondary_deleted_prefixes, additionally
|
53
|
+
# remove other changes with the same prefix.
|
54
|
+
prefix_conflicts = secondary_deleted_prefixes.select do |prefix|
|
55
|
+
path_conflicts.any? { |path| path.start_with?(prefix) }
|
58
56
|
end
|
59
57
|
|
60
|
-
|
61
|
-
|
58
|
+
secondary.delete_if do |path, diff|
|
59
|
+
prefix_conflicts.any? { |prefix| path.start_with?(prefix) }
|
62
60
|
end
|
63
61
|
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
62
|
+
primary.values + secondary.values
|
63
|
+
end
|
64
|
+
|
65
|
+
def apply_diffs(hash, diffs)
|
66
|
+
deep_compact!(HashDiff.patch!(hash, diffs))
|
67
|
+
end
|
68
|
+
|
69
|
+
def resolve!(original:, primary:, secondary:)
|
70
|
+
# To appropriately cope with type changes on either sides, flatten the
|
71
|
+
# trees before calculating the difference and then expand afterwards.
|
72
|
+
f_original = flatten(original)
|
73
|
+
f_primary = flatten(primary)
|
74
|
+
f_secondary = flatten(secondary)
|
75
|
+
|
76
|
+
primary_diffs = HashDiff.diff(f_original, f_primary)
|
77
|
+
secondary_diffs = HashDiff.diff(f_original, f_secondary)
|
78
|
+
|
79
|
+
# However, flattening discards one critical piece of information: when we
|
80
|
+
# have deleted or clobbered an entire prefix (subtree) from the original,
|
81
|
+
# we want to consider this deletion atomic. If any of the changes is
|
82
|
+
# cancelled, they must all be. Motivating example:
|
83
|
+
#
|
84
|
+
# original: { word: { one: "..", "many": ".." } }
|
85
|
+
# primary: { word: { one: "..", "many": "..", "zero": ".." } }
|
86
|
+
# secondary: { word: ".." }
|
87
|
+
# would unexpectedly result in { word: { zero: ".." } }.
|
88
|
+
#
|
89
|
+
# Additionally calculate subtree prefixes that were deleted in `secondary`:
|
90
|
+
secondary_deleted_prefixes =
|
91
|
+
HashDiff.diff(original, secondary, delimiter: SEPARATOR).lazy
|
87
92
|
.select { |op, path, from, to| (op == "-" || op == "~") && from.is_a?(Hash) && !to.is_a?(Hash) }
|
88
93
|
.map { |op, path, from, to| path }
|
89
94
|
.to_a
|
90
95
|
|
91
96
|
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
HashDiff.patch!(f_original, resolved_diffs)
|
97
|
+
resolved_diffs = resolve_diffs(primary: primary_diffs,
|
98
|
+
secondary: secondary_diffs,
|
99
|
+
secondary_deleted_prefixes: secondary_deleted_prefixes)
|
96
100
|
|
97
|
-
|
98
|
-
|
101
|
+
if @verbose
|
102
|
+
STDERR.puts('Primary diffs:')
|
103
|
+
primary_diffs.each { |d| STDERR.puts(d.inspect) }
|
99
104
|
|
105
|
+
STDERR.puts('Secondary diffs:')
|
106
|
+
secondary_diffs.each { |d| STDERR.puts(d.inspect) }
|
100
107
|
|
101
|
-
|
102
|
-
|
103
|
-
def restore_deletions(current, previous)
|
104
|
-
current.deep_merge(previous)
|
108
|
+
STDERR.puts('Resolution:')
|
109
|
+
resolved_diffs.each { |d| STDERR.puts(d.inspect) }
|
105
110
|
end
|
106
111
|
|
107
|
-
|
112
|
+
HashDiff.patch!(f_original, resolved_diffs)
|
113
|
+
|
114
|
+
expand(f_original)
|
115
|
+
end
|
116
|
+
|
117
|
+
|
118
|
+
# Prefer everything in current except deletions,
|
119
|
+
# which are restored from previous if available
|
120
|
+
def restore_deletions(current, previous)
|
121
|
+
current.deep_merge(previous)
|
122
|
+
end
|
123
|
+
|
124
|
+
private
|
108
125
|
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
end
|
126
|
+
def flatten(hash, prefix = nil, acc = {})
|
127
|
+
hash.each do |k, v|
|
128
|
+
k = "#{prefix}#{SEPARATOR}#{k}" if prefix
|
129
|
+
if v.is_a?(Hash)
|
130
|
+
flatten(v, k, acc)
|
131
|
+
else
|
132
|
+
acc[k] = v
|
117
133
|
end
|
118
|
-
acc
|
119
134
|
end
|
135
|
+
acc
|
136
|
+
end
|
120
137
|
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
end
|
128
|
-
raise ArgumentError.new("Type conflict in flattened hash expand: expected no key at #{key}") if leaf.has_key?(leaf_key)
|
129
|
-
leaf[leaf_key] = value
|
138
|
+
def expand(flat_hash)
|
139
|
+
flat_hash.each_with_object({}) do |(key, value), root|
|
140
|
+
path = key.split(SEPARATOR)
|
141
|
+
leaf_key = path.pop
|
142
|
+
leaf = path.inject(root) do |node, path_key|
|
143
|
+
node[path_key] ||= {}
|
130
144
|
end
|
145
|
+
raise ArgumentError.new("Type conflict in flattened hash expand: expected no key at #{key}") if leaf.has_key?(leaf_key)
|
146
|
+
leaf[leaf_key] = value
|
131
147
|
end
|
148
|
+
end
|
132
149
|
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
end
|
140
|
-
parents
|
150
|
+
def path_prefixes(path_string)
|
151
|
+
path = path_string.split(SEPARATOR)
|
152
|
+
parents = []
|
153
|
+
path.inject do |acc, el|
|
154
|
+
parents << acc
|
155
|
+
"#{acc}#{SEPARATOR}#{el}"
|
141
156
|
end
|
157
|
+
parents
|
142
158
|
end
|
143
159
|
end
|
144
160
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: phraseapp_updater
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 2.0.
|
4
|
+
version: 2.0.5
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Kevin Griffin
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2019-02-26 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: thor
|