i18n-tasks 0.5.4 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGES.md +9 -0
  3. data/README.md +46 -36
  4. data/Rakefile +2 -2
  5. data/lib/i18n/tasks/base_task.rb +2 -0
  6. data/lib/i18n/tasks/commands.rb +55 -35
  7. data/lib/i18n/tasks/commands_base.rb +36 -9
  8. data/lib/i18n/tasks/configuration.rb +14 -13
  9. data/lib/i18n/tasks/console_context.rb +0 -1
  10. data/lib/i18n/tasks/data.rb +2 -2
  11. data/lib/i18n/tasks/data/adapter/json_adapter.rb +6 -2
  12. data/lib/i18n/tasks/data/file_formats.rb +19 -4
  13. data/lib/i18n/tasks/data/file_system_base.rb +1 -1
  14. data/lib/i18n/tasks/data/tree/node.rb +11 -37
  15. data/lib/i18n/tasks/data/tree/nodes.rb +6 -2
  16. data/lib/i18n/tasks/data/tree/siblings.rb +53 -29
  17. data/lib/i18n/tasks/data/tree/traversal.rb +4 -2
  18. data/lib/i18n/tasks/fill_tasks.rb +5 -2
  19. data/lib/i18n/tasks/ignore_keys.rb +1 -1
  20. data/lib/i18n/tasks/locale_pathname.rb +1 -1
  21. data/lib/i18n/tasks/logging.rb +2 -0
  22. data/lib/i18n/tasks/missing_keys.rb +74 -31
  23. data/lib/i18n/tasks/plural_keys.rb +4 -3
  24. data/lib/i18n/tasks/reports/base.rb +8 -5
  25. data/lib/i18n/tasks/reports/spreadsheet.rb +15 -3
  26. data/lib/i18n/tasks/reports/terminal.rb +62 -23
  27. data/lib/i18n/tasks/scanners/base_scanner.rb +4 -2
  28. data/lib/i18n/tasks/scanners/relative_keys.rb +21 -0
  29. data/lib/i18n/tasks/split_key.rb +39 -0
  30. data/lib/i18n/tasks/used_keys.rb +1 -1
  31. data/lib/i18n/tasks/version.rb +1 -1
  32. data/spec/fixtures/app/views/index.html.slim +2 -0
  33. data/spec/i18n_tasks_spec.rb +19 -5
  34. data/spec/locale_pathname_spec.rb +24 -0
  35. data/spec/locale_tree/siblings_spec.rb +41 -4
  36. data/spec/split_key_spec.rb +27 -0
  37. data/spec/support/i18n_tasks_output_matcher.rb +7 -13
  38. data/spec/support/trees.rb +4 -0
  39. data/templates/config/i18n-tasks.yml +82 -0
  40. data/templates/rspec/i18n_spec.rb +18 -0
  41. metadata +10 -3
  42. data/lib/i18n/tasks/relative_keys.rb +0 -19
@@ -1,4 +1,5 @@
1
1
  # coding: utf-8
2
+ require 'set'
2
3
  module I18n::Tasks::PluralKeys
3
4
  PLURAL_KEY_SUFFIXES = Set.new %w(zero one two few many other)
4
5
  PLURAL_KEY_RE = /\.(?:#{PLURAL_KEY_SUFFIXES.to_a * '|'})$/
@@ -20,10 +21,10 @@ module I18n::Tasks::PluralKeys
20
21
  # @return the base form if the key is a specific plural form (e.g. apple for apple.many), and the key as passed otherwise
21
22
  def depluralize_key(key, locale = base_locale)
22
23
  return key if key !~ PLURAL_KEY_RE
23
- node = node(key, locale)
24
- nodes = node.try(:siblings).presence || (locale != base_locale && node(key, base_locale).try(:siblings))
24
+ parent_key = split_key(key)[0..-2] * '.'
25
+ nodes = tree("#{locale}.#{parent_key}").presence || (locale != base_locale && tree("#{base_locale}.#{parent_key}"))
25
26
  if nodes && nodes.all? { |x| x.leaf? && ".#{x.key}" =~ PLURAL_KEY_RE }
26
- key.split('.')[0..-2] * '.'
27
+ parent_key
27
28
  else
28
29
  key
29
30
  end
@@ -13,9 +13,8 @@ module I18n::Tasks::Reports
13
13
  protected
14
14
 
15
15
  MISSING_TYPES = {
16
- missing_from_base: {glyph: '✗', summary: 'missing from base locale'},
17
- missing_from_locale: {glyph: '∅', summary: 'missing from locale but present in base locale'},
18
- eq_base: {glyph: '=', summary: 'value equals base value'}
16
+ missing_used: {glyph: '✗', summary: 'used in code but missing from base locale'},
17
+ missing_diff: {glyph: '∅', summary: 'translated in one locale but not in the other'}
19
18
  }
20
19
 
21
20
  def missing_types
@@ -30,11 +29,15 @@ module I18n::Tasks::Reports
30
29
  "Unused keys (#{key_values.count || '∅'})"
31
30
  end
32
31
 
32
+ def eq_base_title(key_values, locale = base_locale)
33
+ "Same value as #{locale} (#{key_values.count || '∅'})"
34
+ end
35
+
33
36
  def used_title(used_tree)
34
37
  leaves = used_tree.leaves.to_a
35
38
  filter = used_tree.first.root.data[:key_filter]
36
39
  used_n = leaves.map { |node| node.data[:source_locations].size }.reduce(:+).to_i
37
- "#{leaves.length} key#{'s' if leaves.size != 1}#{" ~ filter: '#{filter}'" if filter}#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
40
+ "#{leaves.length} key#{'s' if leaves.size != 1}#{" matching '#{filter}'" if filter}#{" (#{used_n} usage#{'s' if used_n != 1})" if used_n > 0}"
38
41
  end
39
42
 
40
43
  # Sort keys by their attributes in order
@@ -50,7 +53,7 @@ module I18n::Tasks::Reports
50
53
 
51
54
  def forest_to_attr(forest)
52
55
  forest.keys(root: false).map { |key, node|
53
- {key: key, type: node.data[:type], locale: node.root.key}
56
+ {key: key, value: node.value, type: node.data[:type], locale: node.root.key, data: node.data}
54
57
  }
55
58
  end
56
59
  end
@@ -10,6 +10,7 @@ module I18n::Tasks::Reports
10
10
  p = Axlsx::Package.new
11
11
  add_missing_sheet p.workbook
12
12
  add_unused_sheet p.workbook
13
+ add_eq_base_sheet p.workbook
13
14
  p.use_shared_strings = true
14
15
  FileUtils.mkpath(File.dirname(path))
15
16
  p.serialize(path)
@@ -37,9 +38,20 @@ module I18n::Tasks::Reports
37
38
  end
38
39
  end
39
40
 
41
+ def add_eq_base_sheet(wb)
42
+ keys = task.eq_base_keys.root_key_values(true)
43
+ add_locale_key_value_table wb, keys, name: eq_base_title(keys)
44
+ end
45
+
40
46
  def add_unused_sheet(wb)
41
- keys = task.unused_keys.root_key_values.sort { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] }
42
- wb.add_worksheet name: unused_title(keys) do |sheet|
47
+ keys = task.unused_keys.root_key_values(true)
48
+ add_locale_key_value_table wb, keys, name: unused_title(keys)
49
+ end
50
+
51
+ private
52
+
53
+ def add_locale_key_value_table(wb, keys, worksheet_opts = {})
54
+ wb.add_worksheet worksheet_opts do |sheet|
43
55
  sheet.add_row ['Locale', 'Key', 'Value']
44
56
  style_header sheet
45
57
  keys.each do |locale_k_v|
@@ -48,7 +60,7 @@ module I18n::Tasks::Reports
48
60
  end
49
61
  end
50
62
 
51
- private
63
+
52
64
  def style_header(sheet)
53
65
  border_bottom = sheet.workbook.styles.add_style(border: {style: :thin, color: '000000', edges: [:bottom]})
54
66
  sheet.rows.first.style = border_bottom
@@ -11,26 +11,21 @@ module I18n
11
11
  print_title missing_title(forest)
12
12
 
13
13
  if forest.present?
14
- print_info "#{bold 'Types:'} #{missing_types.values.map { |t| "#{t[:glyph]} #{t[:summary]}" } * ', '}"
15
- keys_data = sort_by_attr! forest_to_attr(forest), {locale: :asc, type: :asc, key: :asc}
16
- print_table headings: [magenta(bold('Locale')), bold('Type'), magenta(bold 'i18n Key'), bold(cyan "Base value (#{base_locale})")] do |t|
17
- t.rows = keys_data.map do |d|
18
- key = d[:key]
19
- type = d[:type]
20
- locale = d[:locale]
21
- glyph = missing_types[type][:glyph]
22
- glyph = {missing_from_base: red(glyph), missing_from_locale: yellow(glyph), eq_base: bold(blue(glyph))}[type]
23
- if type == :missing_from_base
24
- locale = magenta locale
25
- base_value = ''
14
+ keys_attr = sort_by_attr! forest_to_attr(forest), {locale: :asc, type: :desc, key: :asc}
15
+ print_table headings: [cyan(bold('Locale')), cyan(bold 'Key'), 'Details'] do |t|
16
+ t.rows = keys_attr.map do |a|
17
+ locale, key = a[:locale], a[:key], a[:type]
18
+ if a[:type] == :missing_used
19
+ occ = a[:data][:source_locations]
20
+ first = occ.first
21
+ info = [green("#{first[:src_path]}:#{first[:line_num]}"),
22
+ ("(#{occ.length - 1} more)" if occ.length > 1)].compact.join(' ')
26
23
  else
27
- locale = magenta locale
28
- base_value = task.t(key, base_locale).to_s.strip
24
+ info = a[:value].to_s.strip
29
25
  end
30
- [{value: locale, alignment: :center},
31
- {value: glyph, alignment: :center},
32
- magenta(key),
33
- cyan(base_value)]
26
+ [{value: cyan(locale), alignment: :center},
27
+ cyan(key),
28
+ wrap_string(info, 60)]
34
29
  end
35
30
  end
36
31
  else
@@ -38,6 +33,11 @@ module I18n
38
33
  end
39
34
  end
40
35
 
36
+ def icon(type)
37
+ glyph = missing_types[type][:glyph]
38
+ {missing_used: red(glyph), missing_diff: yellow(glyph)}[type]
39
+ end
40
+
41
41
  def used_keys(used_tree = task.used_tree(source_locations: true))
42
42
  print_title used_title(used_tree)
43
43
  keys_nodes = used_tree.keys.to_a
@@ -60,21 +60,43 @@ module I18n
60
60
  end
61
61
 
62
62
  def unused_keys(tree = task.unused_keys)
63
- keys = tree.root_key_values.sort { |a, b| a[0] != b[0] ? a[0] <=> b[0] : a[1] <=> b[1] }
63
+ keys = tree.root_key_values(true)
64
64
  print_title unused_title(keys)
65
65
  if keys.present?
66
- print_table headings: [bold(magenta('Locale')), bold(magenta('i18n Key')), bold(cyan("Base value (#{base_locale})"))] do |t|
67
- t.rows = keys.map { |(locale, k, v)| [magenta(locale), magenta(k), cyan(v.to_s)] }
68
- end
66
+ print_locale_key_value_table keys
69
67
  else
70
68
  print_success 'Every translation is used!'
71
69
  end
72
70
  end
73
71
 
72
+ def eq_base_keys(tree = task.eq_base_keys)
73
+ keys = tree.root_key_values(true)
74
+ print_title eq_base_title(keys)
75
+ if keys.present?
76
+ print_locale_key_value_table keys
77
+ else
78
+ print_info cyan('No translations are the same as base value')
79
+ end
80
+ end
81
+
82
+ def show_tree(tree)
83
+ print_locale_key_value_table tree.root_key_values(true)
84
+ end
85
+
74
86
  private
75
87
 
88
+ def print_locale_key_value_table(locale_key_values)
89
+ if locale_key_values.present?
90
+ print_table headings: [bold(cyan('Locale')), bold(cyan('Key')), 'Value'] do |t|
91
+ t.rows = locale_key_values.map { |(locale, k, v)| [{value: cyan(locale), alignment: :center}, cyan(k), v.to_s] }
92
+ end
93
+ else
94
+ puts 'ø'
95
+ end
96
+ end
97
+
76
98
  def print_title(title)
77
- log_stderr "#{bold cyan title.strip} #{dark "|"} #{bold "i18n-tasks v#{I18n::Tasks::VERSION}"}"
99
+ log_stderr "#{bold title.strip} #{dark "|"} #{"i18n-tasks v#{I18n::Tasks::VERSION}"}"
78
100
  end
79
101
 
80
102
  def print_success(message)
@@ -97,6 +119,23 @@ module I18n
97
119
  def print_table(opts, &block)
98
120
  puts ::Terminal::Table.new(opts, &block)
99
121
  end
122
+
123
+ def wrap_string(s, max)
124
+ chars = []
125
+ dist = 0
126
+ s.chars.each do |c|
127
+ chars << c
128
+ dist += 1
129
+ if c == "\n"
130
+ dist = 0
131
+ elsif dist == max
132
+ dist = 0
133
+ chars << "\n"
134
+ end
135
+ end
136
+ chars = chars[0..-2] if chars.last == "\n"
137
+ chars.join
138
+ end
100
139
  end
101
140
  end
102
141
  end
@@ -1,9 +1,10 @@
1
1
  # coding: utf-8
2
2
  require 'i18n/tasks/key_pattern_matching'
3
- require 'i18n/tasks/relative_keys'
3
+ require 'i18n/tasks/scanners/relative_keys'
4
+
4
5
  module I18n::Tasks::Scanners
5
6
  class BaseScanner
6
- include ::I18n::Tasks::RelativeKeys
7
+ include RelativeKeys
7
8
  include ::I18n::Tasks::KeyPatternMatching
8
9
  include ::I18n::Tasks::Logging
9
10
 
@@ -11,6 +12,7 @@ module I18n::Tasks::Scanners
11
12
 
12
13
  def initialize(config = {})
13
14
  @config = config.dup.with_indifferent_access.tap do |conf|
15
+ conf[:relative_roots] = %w(app/views) if conf[:relative_roots].blank?
14
16
  conf[:paths] = %w(app/) if conf[:paths].blank?
15
17
  conf[:include] = Array(conf[:include]) if conf[:include].present?
16
18
  if conf.key?(:exclude)
@@ -0,0 +1,21 @@
1
+ # coding: utf-8
2
+ module I18n
3
+ module Tasks
4
+ module Scanners
5
+ module RelativeKeys
6
+ # @param key [String] relative i18n key (starts with a .)
7
+ # @param path [String] path to the file containing the key
8
+ # @return [String] absolute version of the key
9
+ def absolutize_key(key, path, roots = relative_roots)
10
+ # normalized path
11
+ path = File.expand_path path
12
+ (path_root = roots.map { |path| File.expand_path path }.sort.reverse.detect { |root| path.start_with?(root + '/') }) or
13
+ raise CommandError.new("Error scanning #{path}: cannot resolve relative key \"#{key}\".\nSet relative_roots in config/i18n-tasks.yml (currently #{relative_roots.inspect})")
14
+ # key prefix based on path
15
+ prefix = path.gsub(%r(#{path_root}/|(\.[^/]+)*$), '').tr('/', '.').gsub(%r(\._), '.')
16
+ "#{prefix}#{key}"
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,39 @@
1
+ module SplitKey
2
+ extend self
3
+
4
+ # split a key taking parenthesis into account
5
+ # split_key 'a.b' # => ['a', 'b']
6
+ # split_key 'a.#{b.c}' # => ['a', '#{b.c}']
7
+ # split_key 'a.b.c', 2 # => ['a', 'b.c']
8
+ def split_key(key, max = Float::INFINITY)
9
+ parts = []
10
+ nesting = NESTING_CHARS
11
+ counts = Array.new(NESTING_CHARS.size, 0)
12
+ delim = '.'.freeze
13
+ buf = []
14
+ key.to_s.chars.each do |char|
15
+ nest_i, nest_inc = nesting[char]
16
+ if nest_i
17
+ counts[nest_i] += nest_inc
18
+ buf << char
19
+ elsif char == delim && parts.length + 1 < max && counts.all?(&:zero?)
20
+ part = buf.join
21
+ buf.clear
22
+ parts << part
23
+ yield part if block_given?
24
+ else
25
+ buf << char
26
+ end
27
+ end
28
+ parts << buf.join unless buf.empty?
29
+ parts
30
+ end
31
+
32
+ NESTING_CHARS = %w({} [] ()).inject({}) { |h, s|
33
+ i = h.size / 2
34
+ h[s[0].freeze] = [i, 1].freeze
35
+ h[s[1].freeze] = [i, -1].freeze
36
+ h
37
+ }.freeze
38
+ private_constant :NESTING_CHARS
39
+ end
@@ -22,7 +22,7 @@ module I18n::Tasks
22
22
  @scanner ||= begin
23
23
  search_config = (config[:search] || {}).with_indifferent_access
24
24
  class_name = search_config[:scanner] || '::I18n::Tasks::Scanners::PatternWithScopeScanner'
25
- class_name.constantize.new search_config.merge(relative_roots: relative_roots)
25
+ class_name.constantize.new search_config
26
26
  end
27
27
  end
28
28
 
@@ -1,6 +1,6 @@
1
1
  # coding: utf-8
2
2
  module I18n
3
3
  module Tasks
4
- VERSION = '0.5.4'
4
+ VERSION = '0.6.0'
5
5
  end
6
6
  end
@@ -2,6 +2,7 @@
2
2
  | x-t :fp_dash_before
3
3
  / t(:fp_comment)
4
4
  / i18n-tasks-use t(:fn_comment)
5
+ = t 'only_in_es'
5
6
  p #{t('ca.a')} #{t 'ca.b'} #{t "ca.c"}
6
7
  p #{t 'ca.d'} #{t 'ca.f', i: 'world'} #{t 'ca.e', i: 'world'}
7
8
  p #{t 'missing_in_es.a'} #{t 'same_in_es.a'} #{t 'blank_in_es.a'}
@@ -22,3 +23,4 @@ p = t 'missing_in_es_plural_2.a', count: 2
22
23
  p = t 'devise.a'
23
24
  p = t :missing_symbol_key
24
25
  p #{t :"missing_symbol.key_two"} #{t :'missing_symbol.key_three'}
26
+ = t 'present_in_es_but_not_en.a'
@@ -9,15 +9,16 @@ describe 'i18n-tasks' do
9
9
  describe 'missing' do
10
10
  let (:expected_missing_keys) {
11
11
  %w( en.used_but_missing.key en.relative.index.missing
12
- es.missing_in_es.a es.same_in_es.a
12
+ es.missing_in_es.a
13
+ en.present_in_es_but_not_en.a
13
14
  en.hash.pattern_missing.a en.hash.pattern_missing.b
14
15
  en.missing_symbol_key en.missing_symbol.key_two en.missing_symbol.key_three
15
16
  es.missing_in_es_plural_1.a es.missing_in_es_plural_2.a
16
17
  en.missing-key-with-a-dash.key
17
- en.fn_comment
18
+ en.fn_comment en.only_in_es
18
19
  )
19
20
  }
20
- it 'detects missing or identical' do
21
+ it 'detects missing' do
21
22
  capture_stderr do
22
23
  expect(run_cmd :missing).to be_i18n_keys expected_missing_keys
23
24
  es_keys = expected_missing_keys.grep(/^es\./)
@@ -28,6 +29,14 @@ describe 'i18n-tasks' do
28
29
  end
29
30
  end
30
31
 
32
+ describe 'eq_base' do
33
+ it 'detects eq_base' do
34
+ capture_stderr do
35
+ expect(run_cmd :eq_base).to be_i18n_keys %w(es.same_in_es.a)
36
+ end
37
+ end
38
+ end
39
+
31
40
  let(:expected_unused_keys) { %w(unused.a unused.numeric unused.plural).map { |k| %w(en es).map { |l| "#{l}.#{k}" } }.reduce(:+) }
32
41
  describe 'unused' do
33
42
  it 'detects unused' do
@@ -41,7 +50,7 @@ describe 'i18n-tasks' do
41
50
  it 'removes unused' do
42
51
  in_test_app_dir do
43
52
  t = i18n_task
44
- unused = expected_unused_keys.map { |k| k.split('.', 2)[1] }
53
+ unused = expected_unused_keys.map { |k| SplitKey.split_key(k, 2)[1] }
45
54
  unused.each do |key|
46
55
  expect(t.key_value?(key, :en)).to be true
47
56
  expect(t.key_value?(key, :es)).to be true
@@ -88,6 +97,7 @@ describe 'i18n-tasks' do
88
97
  run_cmd :add_missing, locales: 'base'
89
98
  in_test_app_dir {
90
99
  expect(YAML.load_file('config/locales/en.yml')['en']['used_but_missing']['key']).to eq 'Key'
100
+ expect(YAML.load_file('config/locales/en.yml')['en']['present_in_es_but_not_en']['a']).to eq 'A'
91
101
  }
92
102
  end
93
103
 
@@ -111,6 +121,7 @@ describe 'i18n-tasks' do
111
121
  in_test_app_dir {
112
122
  expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']['a']).to eq 'TRME'
113
123
  expect(YAML.load_file('config/locales/devise.es.yml')['es']['devise']['a']).to eq 'ES_TEXT'
124
+ expect(YAML.load_file('config/locales/en.yml')['en']['present_in_es_but_not_en']['a']).to eq 'TRME'
114
125
  }
115
126
  end
116
127
 
@@ -121,6 +132,7 @@ describe 'i18n-tasks' do
121
132
  run_cmd :add_missing, locales: 'all', placeholder: 'TRME %{base_value}'
122
133
  in_test_app_dir {
123
134
  expect(YAML.load_file('config/locales/es.yml')['es']['missing_in_es']['a']).to eq 'TRME EN_TEXT'
135
+ expect(YAML.load_file('config/locales/en.yml')['en']['present_in_es_but_not_en']['a']).to eq 'TRME ES_TEXT'
124
136
  }
125
137
  end
126
138
  end
@@ -148,7 +160,7 @@ used.a 2
148
160
 
149
161
 
150
162
  # --- setup ---
151
- BENCH_KEYS = 100
163
+ BENCH_KEYS = 10
152
164
  before(:each) do
153
165
  gen_data = ->(v) {
154
166
  v_num = v.chars.map(&:ord).join('').to_i
@@ -194,6 +206,8 @@ used.a 2
194
206
  es_data['blank_in_es']['a'] = ''
195
207
  es_data['ignore_eq_base_all']['a'] = 'EN_TEXT'
196
208
  es_data['ignore_eq_base_es']['a'] = 'EN_TEXT'
209
+ es_data['only_in_es'] = 1
210
+ es_data['present_in_es_but_not_en'] = {'a' => 'ES_TEXT'}
197
211
 
198
212
  fs = fixtures_contents.merge(
199
213
  'config/locales/en.yml' => {'en' => en_data}.to_yaml,
@@ -0,0 +1,24 @@
1
+ require 'spec_helper'
2
+
3
+ describe 'LocalePathname' do
4
+ include ::I18n::Tasks::LocalePathname
5
+ context '#replace_locale' do
6
+
7
+ it 'es.yml' do
8
+ expect(replace_locale 'es.yml', 'es', 'fr').to eq 'fr.yml'
9
+ end
10
+
11
+ it 'scope.es.yml' do
12
+ expect(replace_locale 'scope.es.yml', 'es', 'fr').to eq 'scope.fr.yml'
13
+ end
14
+
15
+ it 'path/es.yml' do
16
+ expect(replace_locale 'path/es.yml', 'es', 'fr').to eq 'path/fr.yml'
17
+ end
18
+
19
+ it 'path/scope.es.yml' do
20
+ expect(replace_locale 'path/scope.es.yml', 'es', 'fr').to eq 'path/scope.fr.yml'
21
+ end
22
+
23
+ end
24
+ end
@@ -10,12 +10,12 @@ describe 'Tree siblings / forest' do
10
10
  key: 'fr',
11
11
  children: children
12
12
  )
13
- expect(node.to_siblings.first.children.first.parent.key).to eq 'fr'
13
+ expect(node.to_siblings.first.children.parent.key).to eq 'fr'
14
14
  end
15
15
  end
16
16
 
17
17
  context 'a tree' do
18
- let(:a_hash) { {a: 1, b: {ba: 1, bb: 2}}.deep_stringify_keys }
18
+ let(:a_hash) { {'a' => 1, 'b' => {'ba' => 1, 'bb' => 2}} }
19
19
 
20
20
  it '::from_nested_hash' do
21
21
  a = build_tree(a_hash)
@@ -34,14 +34,34 @@ describe 'Tree siblings / forest' do
34
34
 
35
35
  it '#merge' do
36
36
  a = build_tree(a_hash)
37
- b_hash = {b: {bc: 1}, c: 1}.deep_stringify_keys
37
+ b_hash = {'b' => {'bc' => 1}, 'c' => 1}
38
38
  expect(a.merge(build_tree(b_hash)).to_hash).to eq(a_hash.deep_merge(b_hash))
39
39
  end
40
40
 
41
+ it '#merge does not modify self' do
42
+ a = build_tree(a: 1)
43
+ b = build_tree(a: 2)
44
+ c = a.merge b
45
+ expect(a['a'].value).to eq 1
46
+ expect(c['a'].value).to eq 2
47
+ expect(b['a'].value).to eq 2
48
+ end
49
+
50
+ it '#merge conflict value <- scope' do
51
+ a = build_tree(a: 1)
52
+ b = build_tree(a: {b: 1})
53
+ expect { silence_stderr { a.merge(b) } }.to_not raise_error
54
+ end
55
+
56
+ it '#set conflict value <- scope' do
57
+ a = build_tree(a: 1)
58
+ expect { silence_stderr { a.set('a.b', build_node(key: 'b', value: 1)) } }.to_not raise_error
59
+ end
60
+
41
61
  it '#intersect' do
42
62
  x = {a: 1, b: {ba: 1, bb: 2}}
43
63
  y = {b: {ba: 1, bc: 3}, c: 1}
44
- intersection = {b: {ba: 1}}.deep_stringify_keys
64
+ intersection = {'b' => {'ba' => 1}}
45
65
  a = build_tree(x)
46
66
  b = build_tree(y)
47
67
  expect(a.intersect_keys(b, root: true).to_hash).to eq(intersection)
@@ -50,5 +70,22 @@ describe 'Tree siblings / forest' do
50
70
  it '#select_keys' do
51
71
  expect(build_tree(a: 1, b: 1).select_keys {|k, node| k == 'b'}.to_hash).to eq({'b' => 1})
52
72
  end
73
+
74
+ it '#append!' do
75
+ expect(build_tree({'a' => 1}).append!(build_node(key: 'b', value: 2)).to_hash).to eq('a' => 1, 'b' => 2)
76
+ end
77
+
78
+ it '#set replace value' do
79
+ expect(build_tree(a: {b: 1}).tap {|t| t['a.b'] = build_node(key: 'b', value: 2) }.to_hash).to(
80
+ eq('a' => {'b' => 2})
81
+ )
82
+ end
83
+
84
+ it '#set get' do
85
+ t = build_tree(a: {x: 1})
86
+ node = build_node(key: 'd', value: 'e')
87
+ t['a.b.c.' + node.key] = node
88
+ expect(t['a.b.c.d'].value).to eq('e')
89
+ end
53
90
  end
54
91
  end