phraseapp_updater 2.0.4 → 2.0.5
Sign up to get free protection for your applications and to get access to all the features.
- 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
|