rails_extend 1.0.0 → 1.0.1

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 (55) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +20 -0
  3. data/README.md +46 -0
  4. data/config/locales/en.datetime.yml +17 -0
  5. data/config/locales/zh.datetime.yml +145 -0
  6. data/config/locales/zh.support.yml +52 -0
  7. data/lib/generators/rails_extend/ignore_generator.rb +27 -0
  8. data/lib/generators/rails_extend/migrations_generator.rb +33 -0
  9. data/lib/generators/rails_extend/remove_table_generator.rb +29 -0
  10. data/lib/generators/rails_extend/rename_module_generator.rb +29 -0
  11. data/lib/generators/rails_extend/templates/README.md +2 -0
  12. data/lib/generators/rails_extend/templates/initializer.rb.tt +10 -0
  13. data/lib/generators/rails_extend/templates/migration.rb.tt +35 -0
  14. data/lib/generators/rails_extend/templates/remove_table.rb.tt +9 -0
  15. data/lib/generators/rails_extend/templates/rename_module.rb.tt +9 -0
  16. data/lib/rails_extend/active_model/type_value.rb +26 -0
  17. data/lib/rails_extend/active_model.rb +3 -0
  18. data/lib/rails_extend/active_record/enum.rb +74 -0
  19. data/lib/rails_extend/active_record/extend.rb +200 -0
  20. data/lib/rails_extend/active_record/i18n.rb +31 -0
  21. data/lib/rails_extend/active_record/include.rb +29 -0
  22. data/lib/rails_extend/active_record/taxon.rb +29 -0
  23. data/lib/rails_extend/active_record/translation.rb +41 -0
  24. data/lib/rails_extend/active_record.rb +8 -0
  25. data/lib/rails_extend/active_storage/attachment.rb +16 -0
  26. data/lib/rails_extend/active_storage/blob_prepend.rb +40 -0
  27. data/lib/rails_extend/active_storage/variant.rb +16 -0
  28. data/lib/rails_extend/active_storage.rb +5 -0
  29. data/lib/rails_extend/config.rb +19 -0
  30. data/lib/rails_extend/core/array.rb +79 -0
  31. data/lib/rails_extend/core/date.rb +32 -0
  32. data/lib/rails_extend/core/duration.rb +12 -0
  33. data/lib/rails_extend/core/hash.rb +106 -0
  34. data/lib/rails_extend/core/module.rb +7 -0
  35. data/lib/rails_extend/core/nil.rb +11 -0
  36. data/lib/rails_extend/core/numeric.rb +11 -0
  37. data/lib/rails_extend/core/pathname.rb +15 -0
  38. data/lib/rails_extend/core/string.rb +15 -0
  39. data/lib/rails_extend/core/time_format.rb +20 -0
  40. data/lib/rails_extend/core.rb +17 -0
  41. data/lib/rails_extend/engine.rb +13 -0
  42. data/lib/rails_extend/env.rb +10 -0
  43. data/lib/rails_extend/generators/named_base.rb +22 -0
  44. data/lib/rails_extend/generators.rb +3 -0
  45. data/lib/rails_extend/models.rb +120 -0
  46. data/lib/rails_extend/quiet_logs.rb +22 -0
  47. data/lib/rails_extend/routes.rb +81 -0
  48. data/lib/rails_extend/type/i18n.rb +23 -0
  49. data/lib/rails_extend/type/taxon.rb +15 -0
  50. data/lib/rails_extend/type.rb +4 -0
  51. data/lib/rails_extend.rb +19 -0
  52. data/lib/templates/test_unit/model/fixtures.yml.tt +15 -0
  53. data/lib/templates/test_unit/model/unit_test.rb.tt +7 -0
  54. metadata +56 -5
  55. data/lib/active_model/type_value.rb +0 -20
@@ -0,0 +1,200 @@
1
+ module RailsExtend::ActiveRecord
2
+ module Extend
3
+
4
+ def human_name
5
+ model_name.human
6
+ end
7
+
8
+ def subclasses_tree(tree = {}, node = self)
9
+ tree[node] ||= {}
10
+
11
+ node.subclasses.each do |subclass|
12
+ tree[node].merge! subclasses_tree(tree[node], subclass)
13
+ end
14
+
15
+ tree
16
+ end
17
+
18
+ def to_fixture
19
+ require 'rails/generators/test_unit/model/model_generator'
20
+
21
+ args = [
22
+ self.name.underscore
23
+ ]
24
+ cols = columns.reject(&->(i){ attributes_by_default.include?(i.name) }).map { |col| "#{col.name}:#{col.type}" }
25
+
26
+ generator = TestUnit::Generators::ModelGenerator.new(args + cols, destination_root: Rails.root, fixture: true)
27
+ generator.instance_variable_set :@source_paths, Array(RailsExtend::Engine.root.join('lib/templates', 'test_unit/model'))
28
+ generator.invoke_all
29
+ end
30
+
31
+ def com_column_names
32
+ column_names - attributes_by_default + attachment_reflections.keys
33
+ end
34
+
35
+ def column_attributes
36
+ columns.map do |column|
37
+ r = {
38
+ name: column.name.to_sym,
39
+ type: column.type
40
+ }
41
+ r.merge! null: column.null unless column.null
42
+ r.merge! default: column.default unless column.default.nil?
43
+ r.merge! comment: column.comment if column.comment.present?
44
+ r.merge! column.sql_type_metadata.instance_values.slice('limit', 'precision', 'scale').compact
45
+ r.symbolize_keys!
46
+ end
47
+ end
48
+
49
+ def attributes_by_model
50
+ cols = {}
51
+
52
+ attributes_to_define_after_schema_loads.each do |name, column|
53
+ r = {}
54
+ r.merge! type: column[0]
55
+
56
+ if r[:type].respond_to? :call
57
+ r.merge! type: r[:type].call(ActiveModel::Type::String.new)
58
+ end
59
+
60
+ if r[:type].respond_to?(:type)
61
+ r.merge! raw_type: r[:type].type
62
+ else
63
+ r.merge! raw_type: r[:type] # 兼容 rails 7 以下
64
+ end
65
+
66
+ case r[:type].class.name
67
+ when 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array'
68
+ r.merge! array: true
69
+ when 'ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Range'
70
+ r.merge! range: true
71
+ end
72
+
73
+ if r[:type].respond_to?(:options) && r[:type].options.present?
74
+ r.merge! r[:type].options
75
+ end
76
+
77
+ if Rails::VERSION::MAJOR >= 7 && !column[1].instance_of?(Object) # Rails 7, column[1] 为默认值
78
+ r.merge! default: column[1]
79
+ elsif Rails::VERSION::MAJOR < 7 # rails 7 以下, column[1] 为 options
80
+ r.merge! column[1]
81
+ end
82
+
83
+ cols.merge! name => r
84
+ end
85
+
86
+ cols
87
+ end
88
+
89
+ def migrate_attributes_by_model
90
+ cols = {}
91
+ attributes_by_model.each do |name, column|
92
+ r = {}
93
+ r.merge! column
94
+ r.merge! migrate_type: column[:raw_type]
95
+ r.symbolize_keys!
96
+
97
+ if r[:type].respond_to? :migrate_type
98
+ r.merge! migrate_type: r[:type].migrate_type
99
+ end
100
+
101
+ if r[:default].respond_to?(:call)
102
+ r.delete(:default)
103
+ end
104
+
105
+ if r[:array] && connection.adapter_name != 'PostgreSQL'
106
+ r.delete(:array)
107
+ r[:migrate_type] = :json
108
+ r.delete(:default) if r[:default].is_a? Array
109
+ end
110
+
111
+ if r[:migrate_type] == :json
112
+ if connection.adapter_name == 'PostgreSQL' # Postgres 替换 json 为 jsonb
113
+ r[:migrate_type] = :jsonb
114
+ else
115
+ r.delete(:default) # mysql 数据库不能接受 json 的默认值
116
+ end
117
+ end
118
+
119
+ if r[:migrate_type] == :jsonb && connection.adapter_name != 'PostgreSQL'
120
+ r[:migrate_type] == :json
121
+ r.delete(:default)
122
+ end
123
+
124
+ r.merge! attribute_options: r.slice(:limit, :precision, :scale, :null, :index, :array, :range, :size, :default, :comment).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
125
+
126
+ cols.merge! name => r
127
+ end
128
+
129
+ cols
130
+ end
131
+
132
+ def migrate_attributes_by_db
133
+ return {} unless table_exists?
134
+ cols = {}
135
+
136
+ columns_hash.each do |name, column|
137
+ r = { type: column.type }
138
+ r.merge! migrate_type: r[:type]
139
+ r.merge! null: column.null unless column.null
140
+ r.merge! default: column.default unless column.default.nil?
141
+ r.merge! comment: column.comment if column.comment.present?
142
+ r.merge! column.sql_type_metadata.instance_values.slice('limit', 'precision', 'scale').compact
143
+ r.symbolize_keys!
144
+ r.merge! attribute_options: r.slice(:limit, :precision, :scale, :null, :index, :array, :size, :default, :comment).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
145
+
146
+ cols.merge! name => r
147
+ end
148
+
149
+ cols
150
+ end
151
+
152
+ def attributes_by_default
153
+ if table_exists?
154
+ [primary_key] + all_timestamp_attributes_in_model
155
+ else
156
+ []
157
+ end
158
+ end
159
+
160
+ def attributes_by_belongs
161
+ results = {}
162
+
163
+ reflections.values.select(&->(reflection){ reflection.belongs_to? }).each do |reflection|
164
+ results.merge! reflection.foreign_key => {
165
+ input_type: :integer # todo 考虑 foreign_key 不是自增 ID 的场景
166
+ }
167
+ results.merge! reflection.foreign_type => { input_type: :string } if reflection.foreign_type
168
+ end
169
+ results.except! *attributes_by_model.keys.map(&:to_s)
170
+
171
+ results
172
+ end
173
+
174
+ def references_by_model
175
+ results = {}
176
+ refs = reflections.values.select(&->(reflection){ reflection.belongs_to? })
177
+ refs.reject! { |reflection| reflection.foreign_key.to_s != "#{reflection.name}_id" }
178
+ refs.each do |ref|
179
+ r = { name: ref.name }
180
+ r.merge! polymorphic: true if ref.polymorphic?
181
+ r.merge! reference_options: r.slice(:polymorphic).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
182
+ results[ref.foreign_key] = r unless results.key?(ref.foreign_key.to_sym)
183
+ end
184
+
185
+ results
186
+ end
187
+
188
+ def indexes_by_model
189
+ indexes = indexes_to_define_after_schema_loads
190
+ indexes.map! do |index|
191
+ index.merge! index_options: index.slice(:unique, :name).inject('') { |s, h| s << ", #{h[0]}: #{h[1].inspect}" }
192
+ end
193
+ end
194
+
195
+ end
196
+ end
197
+
198
+ ActiveSupport.on_load :active_record do
199
+ extend RailsExtend::ActiveRecord::Extend
200
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsExtend::ActiveRecord
4
+ module I18n
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_update :update_i18n_column
9
+ end
10
+
11
+ def update_i18n_column
12
+ str = []
13
+ self.changes.slice(*i18n_attributes).each do |key, _|
14
+ value = self.public_send("#{key}_before_type_cast")
15
+ str << "#{key} = #{key}::jsonb || '#{value.to_json}'::jsonb"
16
+ end
17
+ return if str.blank?
18
+ s = str.join(', ')
19
+ self.class.connection.execute "UPDATE #{self.class.table_name} SET #{s} WHERE id = #{self.id}"
20
+ end
21
+
22
+ def attributes_with_values_for_create(attribute_names)
23
+ r = super
24
+ r.slice(*i18n_attributes).each do |key, v|
25
+ r[key] = public_send "#{key}_before_type_cast"
26
+ end
27
+ r
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,29 @@
1
+ module RailsExtend::ActiveRecord
2
+ module Include
3
+ extend ActiveSupport::Concern
4
+
5
+ included do
6
+ class_attribute :indexes_to_define_after_schema_loads, instance_accessor: false, default: []
7
+ end
8
+
9
+ def error_text
10
+ errors.full_messages.join("\n")
11
+ end
12
+
13
+ def class_name
14
+ self.class.base_class.name
15
+ end
16
+
17
+ class_methods do
18
+ def index(name, **options)
19
+ h = { index: name, **options }
20
+ self.indexes_to_define_after_schema_loads = self.indexes_to_define_after_schema_loads + [h]
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+
27
+ ActiveSupport.on_load :active_record do
28
+ include RailsExtend::ActiveRecord::Include
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsExtend::ActiveRecord
4
+ module Taxon
5
+
6
+ def has_taxons(*columns)
7
+ columns.each do |column|
8
+ attribute "#{column}_ancestors", :taxon, outer: column
9
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
10
+ before_validation :sync_#{column}_id, if: -> { #{column}_ancestors_changed? }
11
+
12
+ def sync_#{column}_id
13
+ _outer_id = Hash(#{column}_ancestors).values.compact.last
14
+ if _outer_id
15
+ self.#{column}_id = _outer_id
16
+ else
17
+ self.#{column}_id = nil
18
+ end
19
+ end
20
+ RUBY_EVAL
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+
27
+ ActiveSupport.on_load :active_record do
28
+ extend RailsExtend::ActiveRecord::Taxon
29
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsExtend::ActiveRecord
4
+ module Translation
5
+
6
+ # name
7
+ # * store as jsonb in database;
8
+ # * read with i18n scope
9
+ def has_translations(*columns)
10
+ mattr_accessor :i18n_attributes
11
+ self.i18n_attributes = columns.map(&:to_s)
12
+ include RailsCom::I18n
13
+ columns.each do |column|
14
+ attribute column, :i18n
15
+
16
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
17
+
18
+ def #{column}=(value)
19
+ if value.is_a?(String)
20
+ super(::I18n.locale.to_s => value)
21
+ else
22
+ super
23
+ end
24
+ end
25
+
26
+ RUBY_EVAL
27
+ end
28
+
29
+ end
30
+
31
+ def _update_record(values, constraints)
32
+ values.except!(*i18n_attributes) if self.respond_to?(:i18n_attributes)
33
+ super
34
+ end
35
+
36
+ end
37
+ end
38
+
39
+ ActiveSupport.on_load :active_record do
40
+ extend RailsExtend::ActiveRecord::Translation
41
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_extend/active_record/enum'
4
+ require 'rails_extend/active_record/extend'
5
+ require 'rails_extend/active_record/i18n'
6
+ require 'rails_extend/active_record/include'
7
+ require 'rails_extend/active_record/taxon'
8
+ require 'rails_extend/active_record/translation'
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsExtend::ActiveStorage
4
+ module Attachment
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attribute :name, :string, null: false
9
+ end
10
+
11
+ end
12
+ end
13
+
14
+ ActiveSupport.on_load(:active_storage_blob) do
15
+ ActiveStorage::Attachment.include RailsExtend::ActiveStorage::Attachment
16
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsExtend::ActiveStorage
4
+ module BlobPrepend
5
+
6
+ def self.prepended(klass)
7
+ klass.attribute :key, :string, null: false, index: { unique: true }
8
+ klass.attribute :filename, :string, null: false
9
+ klass.attribute :content_type, :string
10
+ klass.attribute :metadata, :json
11
+ klass.attribute :byte_size, :integer, null: false
12
+ klass.attribute :checksum, :string
13
+ klass.attribute :service_name, :string, null: false
14
+ klass.attribute :created_at, :datetime, null: false
15
+ end
16
+
17
+ def data_url
18
+ return @data_url if defined? @data_url
19
+ @data_url = "data:#{content_type};base64,#{Base64.encode64(download)}"
20
+ end
21
+
22
+ def duration
23
+ (metadata[:duration] || 0).to_f
24
+ end
25
+
26
+ def duration_str
27
+ rh = TimeHelper.exact_distance_time(duration)
28
+ "#{rh[:minute]}:#{rh[:second].to_s.rjust(2, '0')}"
29
+ end
30
+
31
+ def identify_later
32
+ ActiveStorage::IdentifyJob.perform_later(self)
33
+ end
34
+
35
+ end
36
+ end
37
+
38
+ ActiveSupport.on_load(:active_storage_blob) do
39
+ prepend RailsExtend::ActiveStorage::BlobPrepend
40
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailsExtend::ActiveStorage
4
+ module Variant
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ attribute :variation_digest, :string, null: false
9
+ end
10
+
11
+ end
12
+ end
13
+
14
+ ActiveSupport.on_load(:active_storage_blob) do
15
+ ActiveStorage::VariantRecord.include RailsExtend::ActiveStorage::Variant
16
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'rails_extend/active_storage/blob_prepend'
4
+ require 'rails_extend/active_storage/variant' if defined? ActiveStorage::VariantRecord
5
+ require 'rails_extend/active_storage/attachment'
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'active_support/configurable'
4
+
5
+ module RailsExtend #:nodoc:
6
+ include ActiveSupport::Configurable
7
+
8
+ configure do |config|
9
+ config.enum_key = ->(o, attribute){ "#{o.i18n_scope}.enum.#{o.base_class.model_name.i18n_key}.#{attribute}" }
10
+ config.help_key = ->(o, attribute){ "#{o.i18n_scope}.help.#{o.base_class.model_name.i18n_key}.#{attribute}" }
11
+ config.ignore_models = []
12
+ config.quiet_logs = [
13
+ '/rails/active_storage',
14
+ '/images',
15
+ '/@fs'
16
+ ]
17
+ end
18
+
19
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Array
4
+
5
+ # arr = [1, 2, 3]
6
+ # arr.ljust! 5, nil
7
+ # # => [nil, nil, 1, 2, 3]
8
+ def ljust!(n, x)
9
+ return self if n < length
10
+ insert(0, *Array.new([0, n-length].max, x))
11
+ end
12
+
13
+ # fill an array with the given elements left;
14
+ # arr = [1, 2, 3]
15
+ # arr.rjust! 5, nil
16
+ # # => [1, 2, 3, nil, nil]
17
+ def rjust!(n, x)
18
+ return self if n < length
19
+ fill(x, length...n)
20
+ end
21
+
22
+ ##
23
+ # arr = [1, 2, 3]
24
+ # arr.mjust!(5, nil)
25
+ # # => [1, 2, nil, nil, 3]
26
+ def mjust!(n, x)
27
+ return self if n < length
28
+ insert((length / 2.0).ceil, *Array.new(n - length, x))
29
+ end
30
+
31
+ ##
32
+ # combine the same key hash like array
33
+ # raw_data = [
34
+ # { a: 1 },
35
+ # { a: 2 },
36
+ # { b: 2 },
37
+ # { b: 2 }
38
+ # ]
39
+ # raw_data.to_combine_h
40
+ # #=> { a: [1, 2], b: 2 }
41
+ def to_combine_h
42
+ self.inject({}) do |memo, obj|
43
+ memo.merge(obj) do |_, old_val, new_val|
44
+ v = (Array(old_val) + Array(new_val)).uniq
45
+ v.size > 1 ? v : v[0]
46
+ end
47
+ end
48
+ end
49
+
50
+ #
51
+ # raw_data = [
52
+ # [:a, 1],
53
+ # [:a, 2, 3],
54
+ # [:b, 2]
55
+ # ]
56
+ # raw_data.to_array_h
57
+ # #=> [ { a: 1 }, { a: 2 }, { b: 2 } ]
58
+ def to_array_h
59
+ self.map { |x, y| { x => y } }
60
+ end
61
+
62
+ # 2D array to csv file
63
+ # data = [
64
+ # [1, 2],
65
+ # [3, 4]
66
+ # ]
67
+ # data.to_csv_file
68
+ def to_csv_file(file = 'export.csv')
69
+ CSV.open(file, 'w') do |csv|
70
+ self.each { |ar| csv << ar }
71
+ end
72
+ end
73
+
74
+ # 比较两个数组忽略排序的情况下是否相等
75
+ def compare(other)
76
+ (self - other).empty? && (other - self).empty?
77
+ end
78
+
79
+ end
@@ -0,0 +1,32 @@
1
+ class Date
2
+
3
+ # Chinese custom after date
4
+ # '2018-01-01'.to_date.contract_after(2.month) => '2018-02-31'
5
+ # '2018-01-31'.to_date.contract_after(1.month) => '2018-02-28'
6
+ def contract_after(*afters, less: true)
7
+ parts = {}
8
+ # todo use inject or something
9
+ afters.each do |after|
10
+ parts.merge! after.parts
11
+ end
12
+ r = ActiveSupport::Duration.new(0, parts).after(self)
13
+
14
+ return r unless less
15
+
16
+ # if result day less than day, so
17
+ if (parts.keys & [:months, :years]).present?
18
+ r.day < day ? r : r - 1
19
+ else
20
+ r
21
+ end
22
+ end
23
+
24
+ def parts
25
+ {
26
+ year: year,
27
+ month: month,
28
+ day: day
29
+ }
30
+ end
31
+
32
+ end
@@ -0,0 +1,12 @@
1
+ class ActiveSupport::Duration
2
+
3
+ def inspect
4
+ return "#{value} #{I18n.t('duration.seconds')}" if @parts.empty?
5
+
6
+ @parts.
7
+ sort_by { |unit, _ | PARTS.index(unit) }.
8
+ map { |unit, val| "#{val} #{val == 1 ? I18n.t('duration')[unit] : I18n.t('durations')[unit]}" }.
9
+ to_sentence(locale: I18n.locale)
10
+ end
11
+
12
+ end
@@ -0,0 +1,106 @@
1
+ class Hash
2
+
3
+ def toggle(remove = true, other_hash)
4
+ dup.toggle!(remove, other_hash)
5
+ end
6
+
7
+ # a = {a: 1}
8
+ # a.toggle! a: 2
9
+ # => { a: [1, 2] }
10
+ # a.toggle! a: 3
11
+ # => { a: [1, 2, 3] }
12
+ def toggle!(remove = true, other_hash)
13
+ common_keys = self.keys & other_hash.keys
14
+ common_keys.each do |key|
15
+ if remove
16
+ removed = Array(self[key]) & Array(other_hash[key])
17
+ else
18
+ removed = []
19
+ end
20
+ added = Array(other_hash[key]) - Array(self[key])
21
+ self[key] = Array(self[key]) - removed + added
22
+ if self[key].empty?
23
+ self.delete(key)
24
+ elsif self[key].size == 1
25
+ self[key] = self[key][0]
26
+ end
27
+ end
28
+ other_hash.except! *common_keys
29
+ self.merge! other_hash
30
+ self
31
+ end
32
+
33
+ def diff_toggle(remove = true, other_hash)
34
+ removed = {}
35
+ added = {}
36
+ o = other_hash.extract!(*(self.keys & other_hash.keys))
37
+
38
+ if remove
39
+ removed.merge! o.common_basic(self)
40
+ end
41
+ added.merge! o.diff_remove(self)
42
+ added.merge! other_hash
43
+
44
+ [removed, added]
45
+ end
46
+
47
+ # a = { a:1, b: 2 }
48
+ # a.diff_remove { a: [1,2] }
49
+ # => removes: { b: 2 }
50
+ def diff_remove(other_hash, diff = {})
51
+ (keys | other_hash.keys).each do |key|
52
+ if self[key] != other_hash[key]
53
+ if self[key].is_a?(Hash) && other_hash[key].is_a?(Hash)
54
+ diff[key] = self[key].diff_remove(other_hash[key])
55
+ elsif self[key].is_a?(Array) && other_hash[key].is_a?(Array)
56
+ diff[key] = self[key] - other_hash[key]
57
+ elsif other_hash[key].is_a?(NilClass)
58
+ diff[key] = self[key]
59
+ end
60
+ end
61
+ end
62
+
63
+ diff
64
+ end
65
+
66
+ # a = { a:1, b: 2 }
67
+ # a.diff_add { a: [1,2] }
68
+ # => adds: { b: 2 }
69
+ def diff_add(other_hash)
70
+ other_hash.diff_remove(self)
71
+ end
72
+
73
+ def diff_changes(other_hash)
74
+ [diff_remove(other_hash), diff_add(other_hash)]
75
+ end
76
+
77
+ def common_basic(h = {}, other_hash)
78
+ each do |key, value|
79
+ v = Array(value) & Array(other_hash[key])
80
+ if v.size > 1
81
+ h[key] = v
82
+ elsif v.size == 1
83
+ h[key] = v[0]
84
+ elsif v.empty?
85
+ h.delete(key)
86
+ end
87
+ end
88
+ h
89
+ end
90
+
91
+ # 遍历出一个 hash 的根节点
92
+ def leaves
93
+ r = []
94
+
95
+ values.each do |value|
96
+ if value.is_a?(Hash)
97
+ r += value.leaves
98
+ else
99
+ r << value
100
+ end
101
+ end
102
+
103
+ r
104
+ end
105
+
106
+ end
@@ -0,0 +1,7 @@
1
+ class Module
2
+
3
+ def root_module
4
+ name.split('::')[0].constantize
5
+ end
6
+
7
+ end
@@ -0,0 +1,11 @@
1
+ class NilClass
2
+
3
+ def to_d
4
+ BigDecimal(0)
5
+ end
6
+
7
+ def to_sym
8
+ to_s.to_sym
9
+ end
10
+
11
+ end