i18n-tasks 0.5.4 → 0.6.0

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.
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