rails_extend 1.0.0 → 1.0.1

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