fusu 0.1.0 → 0.2.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 02840c743ee74ef741e4749a7911024e50d51c31
4
- data.tar.gz: ac5e6df141ade292c79d4049af983b2d122882e5
3
+ metadata.gz: 5b0df9b35cbcd397ff4eaa1702330e0e0255f84a
4
+ data.tar.gz: 40b28b9e12efa5cf79f00171311c62e3616e0a37
5
5
  SHA512:
6
- metadata.gz: ff735f1620feaa755d65180b8ed80be474e9d4286d208e968998599929f12fa4da68515d409ff5a6bc0ceb1af1cc4a99ab11d0d1e2f226b531ffb13b8b748378
7
- data.tar.gz: 12b3e8a8cb293cb76eec71586bcc5c8e1dd0e64f4870e6f09762a7e304aea00d64f13d09fcc9c184bc501d740cdeee7c52e0c97e31255a012dceaa1b23743b51
6
+ metadata.gz: 028aaafaf603c46939c503d0f4b422fc4649503968d620b950e647f1b0ae4f33dce8d0dd5b7839e58d48bbc5cc6df1e79038c6b9d8478619a5ad5b4b81e34ad1
7
+ data.tar.gz: 28e4994d51bcdb01fd825e0382f5e4c312006a649872443a5ccb31f8ba3a5ff2bf222d86d0e53f40bf846926a644d1b35cb2558cd8f074a24c6d3eb809cec4b2
data/lib/fusu.rb CHANGED
@@ -1,6 +1,12 @@
1
1
  require "fusu/version"
2
2
  require "fusu/blank"
3
+ require "fusu/try"
3
4
  module Fusu
4
5
  extend Blank
6
+ extend Try
5
7
  end
6
8
  require "fusu/array"
9
+ require "fusu/hash"
10
+ require "fusu/string"
11
+ require "fusu/regexp"
12
+ require "fusu/hash_with_indifferent_access"
@@ -0,0 +1,142 @@
1
+ module Fusu
2
+ # A typical module looks like this:
3
+ #
4
+ # module M
5
+ # def self.included(base)
6
+ # base.extend ClassMethods
7
+ # base.class_eval do
8
+ # scope :disabled, -> { where(disabled: true) }
9
+ # end
10
+ # end
11
+ #
12
+ # module ClassMethods
13
+ # ...
14
+ # end
15
+ # end
16
+ #
17
+ # By using <tt>Fusu::Concern</tt> the above module could instead be
18
+ # written as:
19
+ #
20
+ # require 'active_support/concern'
21
+ #
22
+ # module M
23
+ # extend Fusu::Concern
24
+ #
25
+ # included do
26
+ # scope :disabled, -> { where(disabled: true) }
27
+ # end
28
+ #
29
+ # class_methods do
30
+ # ...
31
+ # end
32
+ # end
33
+ #
34
+ # Moreover, it gracefully handles module dependencies. Given a +Foo+ module
35
+ # and a +Bar+ module which depends on the former, we would typically write the
36
+ # following:
37
+ #
38
+ # module Foo
39
+ # def self.included(base)
40
+ # base.class_eval do
41
+ # def self.method_injected_by_foo
42
+ # ...
43
+ # end
44
+ # end
45
+ # end
46
+ # end
47
+ #
48
+ # module Bar
49
+ # def self.included(base)
50
+ # base.method_injected_by_foo
51
+ # end
52
+ # end
53
+ #
54
+ # class Host
55
+ # include Foo # We need to include this dependency for Bar
56
+ # include Bar # Bar is the module that Host really needs
57
+ # end
58
+ #
59
+ # But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
60
+ # could try to hide these from +Host+ directly including +Foo+ in +Bar+:
61
+ #
62
+ # module Bar
63
+ # include Foo
64
+ # def self.included(base)
65
+ # base.method_injected_by_foo
66
+ # end
67
+ # end
68
+ #
69
+ # class Host
70
+ # include Bar
71
+ # end
72
+ #
73
+ # Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
74
+ # is the +Bar+ module, not the +Host+ class. With <tt>Fusu::Concern</tt>,
75
+ # module dependencies are properly resolved:
76
+ #
77
+ # require 'active_support/concern'
78
+ #
79
+ # module Foo
80
+ # extend Fusu::Concern
81
+ # included do
82
+ # def self.method_injected_by_foo
83
+ # ...
84
+ # end
85
+ # end
86
+ # end
87
+ #
88
+ # module Bar
89
+ # extend Fusu::Concern
90
+ # include Foo
91
+ #
92
+ # included do
93
+ # self.method_injected_by_foo
94
+ # end
95
+ # end
96
+ #
97
+ # class Host
98
+ # include Bar # It works, now Bar takes care of its dependencies
99
+ # end
100
+ module Concern
101
+ class MultipleIncludedBlocks < StandardError #:nodoc:
102
+ def initialize
103
+ super "Cannot define multiple 'included' blocks for a Concern"
104
+ end
105
+ end
106
+
107
+ def self.extended(base) #:nodoc:
108
+ base.instance_variable_set(:@_dependencies, [])
109
+ end
110
+
111
+ def append_features(base)
112
+ if base.instance_variable_defined?(:@_dependencies)
113
+ base.instance_variable_get(:@_dependencies) << self
114
+ return false
115
+ else
116
+ return false if base < self
117
+ @_dependencies.each { |dep| base.send(:include, dep) }
118
+ super
119
+ base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
120
+ base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
121
+ end
122
+ end
123
+
124
+ def included(base = nil, &block)
125
+ if base.nil?
126
+ raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
127
+
128
+ @_included_block = block
129
+ else
130
+ super
131
+ end
132
+ end
133
+
134
+ def class_methods(&class_methods_module_definition)
135
+ mod = const_defined?(:ClassMethods, false) ?
136
+ const_get(:ClassMethods) :
137
+ const_set(:ClassMethods, Module.new)
138
+
139
+ mod.module_eval(&class_methods_module_definition)
140
+ end
141
+ end
142
+ end
data/lib/fusu/hash.rb ADDED
@@ -0,0 +1,12 @@
1
+ require 'fusu/hash/except'
2
+ require 'fusu/hash/keys'
3
+ require 'fusu/hash/reverse_merge'
4
+ require 'fusu/hash/deep_merge'
5
+ module Fusu
6
+ module Hash
7
+ extend Except
8
+ extend Keys
9
+ extend ReverseMerge
10
+ extend DeepMerge
11
+ end
12
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+ module Fusu
3
+ module Hash
4
+ module DeepMerge
5
+ # Returns a new hash with +self+ and +other_hash+ merged recursively.
6
+ #
7
+ # h1 = { a: true, b: { c: [1, 2, 3] } }
8
+ # h2 = { a: false, b: { x: [3, 4, 5] } }
9
+ #
10
+ # Fusu::Hash.deep_merge(h1, h2) # => { a: false, b: { c: [1, 2, 3], x: [3, 4, 5] } }
11
+ #
12
+ # Like with Hash#merge in the standard library, a block can be provided
13
+ # to merge values:
14
+ #
15
+ # h1 = { a: 100, b: 200, c: { c1: 100 } }
16
+ # h2 = { b: 250, c: { c1: 200 } }
17
+ # Fusu::Hash.deep_merge(h1, h2) { |key, this_val, other_val| this_val + other_val }
18
+ # # => { a: 100, b: 450, c: { c1: 300 } }
19
+ def deep_merge(hash, other_hash, &block)
20
+ deep_merge!(hash.dup, other_hash, &block)
21
+ end
22
+
23
+ # Same as +deep_merge+, but modifies +self+.
24
+ def deep_merge!(hash, other_hash, &block)
25
+ hash.merge!(other_hash) do |key, this_val, other_val|
26
+ if this_val.class <= ::Hash && other_val.class <= ::Hash
27
+ deep_merge(this_val, other_val, &block)
28
+ elsif block_given?
29
+ block.call(key, this_val, other_val)
30
+ else
31
+ other_val
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,25 @@
1
+ module Fusu
2
+ module Hash
3
+ module Except
4
+ # Returns a hash that includes everything but the given keys.
5
+ # hash = { a: true, b: false, c: nil}
6
+ # Fusu::Hash.except(hash, :c) # => { a: true, b: false}
7
+ # hash # => { a: true, b: false, c: nil}
8
+ #
9
+ # This is useful for limiting a set of parameters to everything but a few known toggles:
10
+ # @person.update(Fusu::Hash.except(params[:person], :admin))
11
+ def except(hash, *keys)
12
+ except!(hash.dup, *keys)
13
+ end
14
+
15
+ # Replaces the hash without the given keys.
16
+ # hash = { a: true, b: false, c: nil}
17
+ # Fusu::Hash.except!(hash, :c) # => { a: true, b: false}
18
+ # hash # => { a: true, b: false }
19
+ def except!(hash, *keys)
20
+ keys.each { |key| hash.delete(key) }
21
+ hash
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,150 @@
1
+ module Fusu
2
+ module Hash
3
+ module Keys
4
+ # Returns a new hash with all keys converted using the block operation.
5
+ #
6
+ # hash = { name: 'Rob', age: '28' }
7
+ #
8
+ # Fusu::Hash.transform_keys(hash){ |key| key.to_s.upcase }
9
+ # # => {"NAME"=>"Rob", "AGE"=>"28"}
10
+ def transform_keys(hash)
11
+ return enum_for(:transform_keys, hash) unless block_given?
12
+ result = ::Hash.new
13
+ hash.each_key do |key|
14
+ result[yield(key)] = hash[key]
15
+ end
16
+ result
17
+ end
18
+
19
+ # Destructively convert all keys using the block operations.
20
+ # Same as transform_keys but modifies +self+.
21
+ def transform_keys!(hash)
22
+ return enum_for(:transform_keys!, hash) unless block_given?
23
+ hash.keys.each do |key|
24
+ hash[yield(key)] = hash.delete(key)
25
+ end
26
+ hash
27
+ end
28
+
29
+ # Returns a new hash with all keys converted to strings.
30
+ #
31
+ # hash = { name: 'Rob', age: '28' }
32
+ #
33
+ # Fusu::Hash.stringify_keys(hash)
34
+ # # => {"name"=>"Rob", "age"=>"28"}
35
+ def stringify_keys(hash)
36
+ transform_keys(hash){ |key| key.to_s }
37
+ end
38
+
39
+ # Destructively convert all keys to strings. Same as
40
+ # +stringify_keys+, but modifies +self+.
41
+ def stringify_keys!(hash)
42
+ transform_keys!(hash){ |key| key.to_s }
43
+ end
44
+
45
+ # Returns a new hash with all keys converted to symbols, as long as
46
+ # they respond to +to_sym+.
47
+ #
48
+ # hash = { 'name' => 'Rob', 'age' => '28' }
49
+ #
50
+ # Fusu::Hash.symbolize_keys(hash)
51
+ # # => {:name=>"Rob", :age=>"28"}
52
+ def symbolize_keys(hash)
53
+ transform_keys(hash){ |key| key.to_sym rescue key }
54
+ end
55
+ alias_method :to_options, :symbolize_keys
56
+
57
+ # Destructively convert all keys to symbols, as long as they respond
58
+ # to +to_sym+. Same as +symbolize_keys+, but modifies +self+.
59
+ def symbolize_keys!(hash)
60
+ transform_keys!(hash){ |key| key.to_sym rescue key }
61
+ end
62
+ alias_method :to_options!, :symbolize_keys!
63
+
64
+ # Returns a new hash with all keys converted by the block operation.
65
+ # This includes the keys from the root hash and from all
66
+ # nested hashes and arrays.
67
+ #
68
+ # hash = { person: { name: 'Rob', age: '28' } }
69
+ #
70
+ # Fusu::Hash.deep_transform_keys(hash){ |key| key.to_s.upcase }
71
+ # # => {"PERSON"=>{"NAME"=>"Rob", "AGE"=>"28"}}
72
+ def deep_transform_keys(hash, &block)
73
+ _deep_transform_keys_in_object(hash, &block)
74
+ end
75
+
76
+ # Destructively convert all keys by using the block operation.
77
+ # This includes the keys from the root hash and from all
78
+ # nested hashes and arrays.
79
+ def deep_transform_keys!(hash, &block)
80
+ _deep_transform_keys_in_object!(hash, &block)
81
+ end
82
+
83
+ # Returns a new hash with all keys converted to strings.
84
+ # This includes the keys from the root hash and from all
85
+ # nested hashes and arrays.
86
+ #
87
+ # hash = { person: { name: 'Rob', age: '28' } }
88
+ #
89
+ # Fusu::Hash.deep_stringify_keys(hash)
90
+ # # => {"person"=>{"name"=>"Rob", "age"=>"28"}}
91
+ def deep_stringify_keys(hash)
92
+ deep_transform_keys(hash){ |key| key.to_s }
93
+ end
94
+
95
+ # Destructively convert all keys to strings.
96
+ # This includes the keys from the root hash and from all
97
+ # nested hashes and arrays.
98
+ def deep_stringify_keys!(hash)
99
+ deep_transform_keys!(hash){ |key| key.to_s }
100
+ end
101
+
102
+ # Returns a new hash with all keys converted to symbols, as long as
103
+ # they respond to +to_sym+. This includes the keys from the root hash
104
+ # and from all nested hashes and arrays.
105
+ #
106
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
107
+ #
108
+ # Fusu::Hash.deep_symbolize_keys(hash)
109
+ # # => {:person=>{:name=>"Rob", :age=>"28"}}
110
+ def deep_symbolize_keys(hash)
111
+ deep_transform_keys(hash){ |key| key.to_sym rescue key }
112
+ end
113
+
114
+ # Destructively convert all keys to symbols, as long as they respond
115
+ # to +to_sym+. This includes the keys from the root hash and from all
116
+ # nested hashes and arrays.
117
+ def deep_symbolize_keys!(hash)
118
+ deep_transform_keys!(hash){ |key| key.to_sym rescue key }
119
+ end
120
+
121
+ private
122
+ # support methods for deep transforming nested hashes and arrays
123
+ def _deep_transform_keys_in_object(object, &block)
124
+ if object.class <= ::Hash
125
+ object.each_with_object({}) do |(key, value), result|
126
+ result[yield(key)] = _deep_transform_keys_in_object(value, &block)
127
+ end
128
+ elsif object.class <= ::Array
129
+ object.map {|e| _deep_transform_keys_in_object(e, &block) }
130
+ else
131
+ object
132
+ end
133
+ end
134
+
135
+ def _deep_transform_keys_in_object!(object, &block)
136
+ if object.class <= ::Hash
137
+ object.keys.each do |key|
138
+ value = object.delete(key)
139
+ object[yield(key)] = _deep_transform_keys_in_object!(value, &block)
140
+ end
141
+ object
142
+ elsif object.class <= ::Array
143
+ object.map! {|e| _deep_transform_keys_in_object!(e, &block)}
144
+ else
145
+ object
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,26 @@
1
+ module Fusu
2
+ module Hash
3
+ module ReverseMerge
4
+ # Merges the caller into +other_hash+. For example,
5
+ #
6
+ # options = Fusu::Hash.reverse_merge(options, {size: 25, velocity: 10})
7
+ #
8
+ # is equivalent to
9
+ #
10
+ # options = { size: 25, velocity: 10 }.merge(options)
11
+ #
12
+ # This is particularly useful for initializing an options hash
13
+ # with default values.
14
+ def reverse_merge(hash, other_hash)
15
+ other_hash.merge(hash)
16
+ end
17
+
18
+ # Destructive +reverse_merge+.
19
+ def reverse_merge!(hash, other_hash)
20
+ # right wins if there is no left
21
+ hash.merge!( other_hash ){|key,left,right| left }
22
+ end
23
+ alias_method :reverse_update, :reverse_merge!
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,288 @@
1
+ require 'fusu/hash/keys'
2
+ require 'fusu/hash/reverse_merge'
3
+
4
+ module Fusu
5
+ # Implements a hash where keys <tt>:foo</tt> and <tt>"foo"</tt> are considered
6
+ # to be the same.
7
+ #
8
+ # rgb = Fusu::HashWithIndifferentAccess.new
9
+ #
10
+ # rgb[:black] = '#000000'
11
+ # rgb[:black] # => '#000000'
12
+ # rgb['black'] # => '#000000'
13
+ #
14
+ # rgb['white'] = '#FFFFFF'
15
+ # rgb[:white] # => '#FFFFFF'
16
+ # rgb['white'] # => '#FFFFFF'
17
+ #
18
+ # Internally symbols are mapped to strings when used as keys in the entire
19
+ # writing interface (calling <tt>[]=</tt>, <tt>merge</tt>, etc). This
20
+ # mapping belongs to the public interface. For example, given:
21
+ #
22
+ # hash = Fusu::HashWithIndifferentAccess.new(a: 1)
23
+ #
24
+ # You are guaranteed that the key is returned as a string:
25
+ #
26
+ # hash.keys # => ["a"]
27
+ #
28
+ # Technically other types of keys are accepted:
29
+ #
30
+ # hash = Fusu::HashWithIndifferentAccess.new(a: 1)
31
+ # hash[0] = 0
32
+ # hash # => {"a"=>1, 0=>0}
33
+ #
34
+ # but this class is intended for use cases where strings or symbols are the
35
+ # expected keys and it is convenient to understand both as the same. For
36
+ # example the +params+ hash in Ruby on Rails.
37
+ #
38
+ # Note that core extensions define <tt>Hash#with_indifferent_access</tt>:
39
+ #
40
+ # rgb = { black: '#000000', white: '#FFFFFF' }.with_indifferent_access
41
+ #
42
+ # which may be handy.
43
+ class HashWithIndifferentAccess < ::Hash
44
+ # Returns +true+ so that <tt>Array#extract_options!</tt> finds members of
45
+ # this class.
46
+ def extractable_options?
47
+ true
48
+ end
49
+
50
+ def with_indifferent_access
51
+ dup
52
+ end
53
+
54
+ def nested_under_indifferent_access
55
+ self
56
+ end
57
+
58
+ def initialize(constructor = {})
59
+
60
+ if constructor.respond_to?(:to_hash)
61
+ super()
62
+ update(constructor)
63
+ else
64
+ super(constructor)
65
+ end
66
+ end
67
+
68
+ def default(key = nil)
69
+ if key.is_a?(Symbol) && include?(key = key.to_s)
70
+ self[key]
71
+ else
72
+ super
73
+ end
74
+ end
75
+
76
+ def self.new_from_hash_copying_default(hash)
77
+ hash = hash.to_hash
78
+ new(hash).tap do |new_hash|
79
+ new_hash.default = hash.default
80
+ new_hash.default_proc = hash.default_proc if hash.default_proc
81
+ end
82
+ end
83
+
84
+ def self.[](*args)
85
+ new.merge!(::Hash[*args])
86
+ end
87
+
88
+ alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
89
+ alias_method :regular_update, :update unless method_defined?(:regular_update)
90
+
91
+ # Assigns a new value to the hash:
92
+ #
93
+ # hash = Fusu::HashWithIndifferentAccess.new
94
+ # hash[:key] = 'value'
95
+ #
96
+ # This value can be later fetched using either +:key+ or +'key'+.
97
+ def []=(key, value)
98
+ regular_writer(convert_key(key), convert_value(value, for: :assignment))
99
+ end
100
+
101
+ alias_method :store, :[]=
102
+
103
+ # Updates the receiver in-place, merging in the hash passed as argument:
104
+ #
105
+ # hash_1 = Fusu::HashWithIndifferentAccess.new
106
+ # hash_1[:key] = 'value'
107
+ #
108
+ # hash_2 = Fusu::HashWithIndifferentAccess.new
109
+ # hash_2[:key] = 'New Value!'
110
+ #
111
+ # hash_1.update(hash_2) # => {"key"=>"New Value!"}
112
+ #
113
+ # The argument can be either an
114
+ # <tt>Fusu::HashWithIndifferentAccess</tt> or a regular +Hash+.
115
+ # In either case the merge respects the semantics of indifferent access.
116
+ #
117
+ # If the argument is a regular hash with keys +:key+ and +"key"+ only one
118
+ # of the values end up in the receiver, but which one is unspecified.
119
+ #
120
+ # When given a block, the value for duplicated keys will be determined
121
+ # by the result of invoking the block with the duplicated key, the value
122
+ # in the receiver, and the value in +other_hash+. The rules for duplicated
123
+ # keys follow the semantics of indifferent access:
124
+ #
125
+ # hash_1[:key] = 10
126
+ # hash_2['key'] = 12
127
+ # hash_1.update(hash_2) { |key, old, new| old + new } # => {"key"=>22}
128
+ def update(other_hash)
129
+ if other_hash.is_a? HashWithIndifferentAccess
130
+ super(other_hash)
131
+ else
132
+ other_hash.to_hash.each_pair do |key, value|
133
+ if block_given? && key?(key)
134
+ value = yield(convert_key(key), self[key], value)
135
+ end
136
+ regular_writer(convert_key(key), convert_value(value))
137
+ end
138
+ self
139
+ end
140
+ end
141
+
142
+ alias_method :merge!, :update
143
+
144
+ # Checks the hash for a key matching the argument passed in:
145
+ #
146
+ # hash = Fusu::HashWithIndifferentAccess.new
147
+ # hash['key'] = 'value'
148
+ # hash.key?(:key) # => true
149
+ # hash.key?('key') # => true
150
+ def key?(key)
151
+ super(convert_key(key))
152
+ end
153
+
154
+ alias_method :include?, :key?
155
+ alias_method :has_key?, :key?
156
+ alias_method :member?, :key?
157
+
158
+ # Same as <tt>Hash#fetch</tt> where the key passed as argument can be
159
+ # either a string or a symbol:
160
+ #
161
+ # counters = Fusu::HashWithIndifferentAccess.new
162
+ # counters[:foo] = 1
163
+ #
164
+ # counters.fetch('foo') # => 1
165
+ # counters.fetch(:bar, 0) # => 0
166
+ # counters.fetch(:bar) { |key| 0 } # => 0
167
+ # counters.fetch(:zoo) # => KeyError: key not found: "zoo"
168
+ def fetch(key, *extras)
169
+ super(convert_key(key), *extras)
170
+ end
171
+
172
+ # Returns an array of the values at the specified indices:
173
+ #
174
+ # hash = Fusu::HashWithIndifferentAccess.new
175
+ # hash[:a] = 'x'
176
+ # hash[:b] = 'y'
177
+ # hash.values_at('a', 'b') # => ["x", "y"]
178
+ def values_at(*indices)
179
+ indices.collect { |key| self[convert_key(key)] }
180
+ end
181
+
182
+ # Returns a shallow copy of the hash.
183
+ #
184
+ # hash = Fusu::HashWithIndifferentAccess.new({ a: { b: 'b' } })
185
+ # dup = hash.dup
186
+ # dup[:a][:c] = 'c'
187
+ #
188
+ # hash[:a][:c] # => nil
189
+ # dup[:a][:c] # => "c"
190
+ def dup
191
+ self.class.new(self).tap do |new_hash|
192
+ set_defaults(new_hash)
193
+ end
194
+ end
195
+
196
+ # This method has the same semantics of +update+, except it does not
197
+ # modify the receiver but rather returns a new hash with indifferent
198
+ # access with the result of the merge.
199
+ def merge(hash, &block)
200
+ self.dup.update(hash, &block)
201
+ end
202
+
203
+ # Like +merge+ but the other way around: Merges the receiver into the
204
+ # argument and returns a new hash with indifferent access as result:
205
+ #
206
+ # hash = Fusu::HashWithIndifferentAccess.new
207
+ # hash['a'] = nil
208
+ # hash.reverse_merge(a: 0, b: 1) # => {"a"=>nil, "b"=>1}
209
+ def reverse_merge(other_hash)
210
+ super(self.class.new_from_hash_copying_default(other_hash))
211
+ end
212
+
213
+ # Same semantics as +reverse_merge+ but modifies the receiver in-place.
214
+ def reverse_merge!(other_hash)
215
+ replace(Fusu::Hash.reverse_merge(self, other_hash ))
216
+ end
217
+
218
+ # Replaces the contents of this hash with other_hash.
219
+ #
220
+ # h = { "a" => 100, "b" => 200 }
221
+ # h.replace({ "c" => 300, "d" => 400 }) # => {"c"=>300, "d"=>400}
222
+ def replace(other_hash)
223
+ super(self.class.new_from_hash_copying_default(other_hash))
224
+ end
225
+
226
+ # Removes the specified key from the hash.
227
+ def delete(key)
228
+ super(convert_key(key))
229
+ end
230
+
231
+ def stringify_keys!; self end
232
+ def deep_stringify_keys!; self end
233
+ def stringify_keys; dup end
234
+ def deep_stringify_keys; dup end
235
+ def symbolize_keys; Fusu::Hash.symbolize_keys!(to_hash) end
236
+ def deep_symbolize_keys; Fusu::Hash.deep_symbolize_keys!(to_hash) end
237
+ def to_options!; self end
238
+
239
+ def select(*args, &block)
240
+ dup.tap { |hash| hash.select!(*args, &block) }
241
+ end
242
+
243
+ def reject(*args, &block)
244
+ dup.tap { |hash| hash.reject!(*args, &block) }
245
+ end
246
+
247
+ # Convert to a regular hash with string keys.
248
+ def to_hash
249
+ _new_hash = ::Hash.new
250
+ set_defaults(_new_hash)
251
+
252
+ each do |key, value|
253
+ _new_hash[key] = convert_value(value, for: :to_hash)
254
+ end
255
+ _new_hash
256
+ end
257
+
258
+ protected
259
+ def convert_key(key)
260
+ key.kind_of?(Symbol) ? key.to_s : key
261
+ end
262
+
263
+ def convert_value(value, options = {})
264
+ if value.is_a? ::Hash
265
+ if options[:for] == :to_hash
266
+ value.to_hash
267
+ else
268
+ self.class.new(value)
269
+ end
270
+ elsif value.is_a?(::Array)
271
+ if options[:for] != :assignment || value.frozen?
272
+ value = value.dup
273
+ end
274
+ value.map! { |e| convert_value(e, options) }
275
+ else
276
+ value
277
+ end
278
+ end
279
+
280
+ def set_defaults(target)
281
+ if default_proc
282
+ target.default_proc = default_proc.dup
283
+ else
284
+ target.default = default
285
+ end
286
+ end
287
+ end
288
+ end
@@ -0,0 +1,12 @@
1
+ module Fusu
2
+ module Regexp #:nodoc:
3
+ extend self
4
+ # def multiline?(regexp)
5
+ # regexp.options & MULTILINE == MULTILINE
6
+ # end
7
+
8
+ def match?(regexp, string, pos = 0)
9
+ !!regexp.match(string, pos)
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,6 @@
1
+ require 'fusu/string/methods'
2
+ module Fusu
3
+ module String
4
+ extend Methods
5
+ end
6
+ end
@@ -0,0 +1,348 @@
1
+ # frozen_string_literal: true
2
+ module Fusu
3
+ module String
4
+ module Methods
5
+ ACRONYM_REGEX = /(?=a)b/
6
+ # The Inflector transforms words from singular to plural, class names to table
7
+ # names, modularized class names to ones without, and class names to foreign
8
+ # keys. The default inflections for pluralization, singularization, and
9
+ # uncountable words are kept in inflections.rb.
10
+ #
11
+ # The Rails core team has stated patches for the inflections library will not
12
+ # be accepted in order to avoid breaking legacy applications which may be
13
+ # relying on errant inflections. If you discover an incorrect inflection and
14
+ # require it for your application or wish to define rules for languages other
15
+ # than English, please correct or add them yourself (explained below).
16
+
17
+ # Converts strings to UpperCamelCase.
18
+ # If the +uppercase_first_letter+ parameter is set to false, then produces
19
+ # lowerCamelCase.
20
+ #
21
+ # Also converts '/' to '::' which is useful for converting
22
+ # paths to namespaces.
23
+ #
24
+ # camelize('active_model') # => "ActiveModel"
25
+ # camelize('active_model', false) # => "activeModel"
26
+ # camelize('active_model/errors') # => "ActiveModel::Errors"
27
+ # camelize('active_model/errors', false) # => "activeModel::Errors"
28
+ #
29
+ # As a rule of thumb you can think of +camelize+ as the inverse of
30
+ # #underscore, though there are cases where that does not hold:
31
+ #
32
+ # camelize(underscore('SSLError')) # => "SslError"
33
+ def camelize(term, uppercase_first_letter = true)
34
+ string = term.to_s
35
+ if uppercase_first_letter
36
+ string = string.sub(/^[a-z\d]*/) { |match| match.capitalize }
37
+ else
38
+ string = string.sub(/^(?:#{ACRONYM_REGEX}(?=\b|[A-Z_])|\w)/) { |match| match.downcase }
39
+ end
40
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
41
+ string.gsub!("/".freeze, "::".freeze)
42
+ string
43
+ end
44
+
45
+ # Makes an underscored, lowercase form from the expression in the string.
46
+ #
47
+ # Changes '::' to '/' to convert namespaces to paths.
48
+ #
49
+ # underscore('ActiveModel') # => "active_model"
50
+ # underscore('ActiveModel::Errors') # => "active_model/errors"
51
+ #
52
+ # As a rule of thumb you can think of +underscore+ as the inverse of
53
+ # #camelize, though there are cases where that does not hold:
54
+ #
55
+ # camelize(underscore('SSLError')) # => "SslError"
56
+ def underscore(camel_cased_word)
57
+ return camel_cased_word unless Fusu::Regexp.match?(/[A-Z-]|::/, camel_cased_word)
58
+ word = camel_cased_word.to_s.gsub("::".freeze, "/".freeze)
59
+ word.gsub!(/(?:(?<=([A-Za-z\d]))|\b)(#{ACRONYM_REGEX})(?=\b|[^a-z])/) { "#{$1 && '_'.freeze }" }
60
+ word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2'.freeze)
61
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2'.freeze)
62
+ word.tr!("-".freeze, "_".freeze)
63
+ word.downcase!
64
+ word
65
+ end
66
+
67
+ # Converts just the first character to uppercase.
68
+ #
69
+ # upcase_first('what a Lovely Day') # => "What a Lovely Day"
70
+ # upcase_first('w') # => "W"
71
+ # upcase_first('') # => ""
72
+ def upcase_first(string)
73
+ string.length > 0 ? string[0].upcase.concat(string[1..-1]) : ""
74
+ end
75
+
76
+ # Tweaks an attribute name for display to end users.
77
+ #
78
+ # Specifically, performs these transformations:
79
+ #
80
+ # * Applies human inflection rules to the argument.
81
+ # * Deletes leading underscores, if any.
82
+ # * Removes a "_id" suffix if present.
83
+ # * Replaces underscores with spaces, if any.
84
+ # * Downcases all words except acronyms.
85
+ # * Capitalizes the first word.
86
+ # The capitalization of the first word can be turned off by setting the
87
+ # +:capitalize+ option to false (default is true).
88
+ #
89
+ # The trailing '_id' can be kept and capitalized by setting the
90
+ # optional parameter +keep_id_suffix+ to true (default is false).
91
+ #
92
+ # humanize('employee_salary') # => "Employee salary"
93
+ # humanize('author_id') # => "Author"
94
+ # humanize('author_id', capitalize: false) # => "author"
95
+ # humanize('_id') # => "Id"
96
+ # humanize('author_id', keep_id_suffix: true) # => "Author Id"
97
+ #
98
+ # If "SSL" was defined to be an acronym:
99
+ #
100
+ # humanize('ssl_error') # => "SSL error"
101
+ #
102
+ def humanize(lower_case_and_underscored_word, capitalize: true, keep_id_suffix: false)
103
+ result = lower_case_and_underscored_word.to_s.dup
104
+
105
+ # inflections.humans.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
106
+
107
+ result.sub!(/\A_+/, "".freeze)
108
+ unless keep_id_suffix
109
+ result.sub!(/_id\z/, "".freeze)
110
+ end
111
+ result.tr!("_".freeze, " ".freeze)
112
+
113
+ result.gsub!(/([a-z\d]*)/i) do |match|
114
+ "#{match.downcase}"
115
+ end
116
+
117
+ if capitalize
118
+ result.sub!(/\A\w/) { |match| match.upcase }
119
+ end
120
+
121
+ result
122
+ end
123
+
124
+ # Capitalizes all the words and replaces some characters in the string to
125
+ # create a nicer looking title. +titleize+ is meant for creating pretty
126
+ # output. It is not used in the Rails internals.
127
+ #
128
+ # The trailing '_id','Id'.. can be kept and capitalized by setting the
129
+ # optional parameter +keep_id_suffix+ to true.
130
+ # By default, this parameter is false.
131
+ #
132
+ # +titleize+ is also aliased as +titlecase+.
133
+ #
134
+ # titleize('man from the boondocks') # => "Man From The Boondocks"
135
+ # titleize('x-men: the last stand') # => "X Men: The Last Stand"
136
+ # titleize('TheManWithoutAPast') # => "The Man Without A Past"
137
+ # titleize('raiders_of_the_lost_ark') # => "Raiders Of The Lost Ark"
138
+ # titleize('string_ending_with_id', keep_id_suffix: true) # => "String Ending With Id"
139
+ def titleize(word, keep_id_suffix: false)
140
+ humanize(underscore(word), keep_id_suffix: keep_id_suffix).gsub(/\b(?<!\w['’`])[a-z]/) do |match|
141
+ match.capitalize
142
+ end
143
+ end
144
+
145
+ # Replaces underscores with dashes in the string.
146
+ #
147
+ # dasherize('puni_puni') # => "puni-puni"
148
+ def dasherize(underscored_word)
149
+ underscored_word.tr("_".freeze, "-".freeze)
150
+ end
151
+
152
+ # Removes the module part from the expression in the string.
153
+ #
154
+ # demodulize('ActiveSupport::Inflector::Inflections') # => "Inflections"
155
+ # demodulize('Inflections') # => "Inflections"
156
+ # demodulize('::Inflections') # => "Inflections"
157
+ # demodulize('') # => ""
158
+ #
159
+ # See also #deconstantize.
160
+ def demodulize(path)
161
+ path = path.to_s
162
+ if i = path.rindex("::")
163
+ path[(i + 2)..-1]
164
+ else
165
+ path
166
+ end
167
+ end
168
+
169
+ # Removes the rightmost segment from the constant expression in the string.
170
+ #
171
+ # deconstantize('Net::HTTP') # => "Net"
172
+ # deconstantize('::Net::HTTP') # => "::Net"
173
+ # deconstantize('String') # => ""
174
+ # deconstantize('::String') # => ""
175
+ # deconstantize('') # => ""
176
+ #
177
+ # See also #demodulize.
178
+ def deconstantize(path)
179
+ path.to_s[0, path.rindex("::") || 0] # implementation based on the one in facets' Module#spacename
180
+ end
181
+
182
+ # Creates a foreign key name from a class name.
183
+ # +separate_class_name_and_id_with_underscore+ sets whether
184
+ # the method should put '_' between the name and 'id'.
185
+ #
186
+ # foreign_key('Message') # => "message_id"
187
+ # foreign_key('Message', false) # => "messageid"
188
+ # foreign_key('Admin::Post') # => "post_id"
189
+ def foreign_key(class_name, separate_class_name_and_id_with_underscore = true)
190
+ underscore(demodulize(class_name)) + (separate_class_name_and_id_with_underscore ? "_id" : "id")
191
+ end
192
+
193
+ # Tries to find a constant with the name specified in the argument string.
194
+ #
195
+ # constantize('Module') # => Module
196
+ # constantize('Foo::Bar') # => Foo::Bar
197
+ #
198
+ # The name is assumed to be the one of a top-level constant, no matter
199
+ # whether it starts with "::" or not. No lexical context is taken into
200
+ # account:
201
+ #
202
+ # C = 'outside'
203
+ # module M
204
+ # C = 'inside'
205
+ # C # => 'inside'
206
+ # constantize('C') # => 'outside', same as ::C
207
+ # end
208
+ #
209
+ # NameError is raised when the name is not in CamelCase or the constant is
210
+ # unknown.
211
+ def constantize(camel_cased_word)
212
+ names = camel_cased_word.split("::".freeze)
213
+
214
+ # Trigger a built-in NameError exception including the ill-formed constant in the message.
215
+ Object.const_get(camel_cased_word) if names.empty?
216
+
217
+ # Remove the first blank element in case of '::ClassName' notation.
218
+ names.shift if names.size > 1 && names.first.empty?
219
+
220
+ names.inject(Object) do |constant, name|
221
+ if constant == Object
222
+ constant.const_get(name)
223
+ else
224
+ candidate = constant.const_get(name)
225
+ next candidate if constant.const_defined?(name, false)
226
+ next candidate unless Object.const_defined?(name)
227
+
228
+ # Go down the ancestors to check if it is owned directly. The check
229
+ # stops when we reach Object or the end of ancestors tree.
230
+ constant = constant.ancestors.inject(constant) do |const, ancestor|
231
+ break const if ancestor == Object
232
+ break ancestor if ancestor.const_defined?(name, false)
233
+ const
234
+ end
235
+
236
+ # owner is in Object, so raise
237
+ constant.const_get(name, false)
238
+ end
239
+ end
240
+ end
241
+
242
+ # Tries to find a constant with the name specified in the argument string.
243
+ #
244
+ # safe_constantize('Module') # => Module
245
+ # safe_constantize('Foo::Bar') # => Foo::Bar
246
+ #
247
+ # The name is assumed to be the one of a top-level constant, no matter
248
+ # whether it starts with "::" or not. No lexical context is taken into
249
+ # account:
250
+ #
251
+ # C = 'outside'
252
+ # module M
253
+ # C = 'inside'
254
+ # C # => 'inside'
255
+ # safe_constantize('C') # => 'outside', same as ::C
256
+ # end
257
+ #
258
+ # +nil+ is returned when the name is not in CamelCase or the constant (or
259
+ # part of it) is unknown.
260
+ #
261
+ # safe_constantize('blargle') # => nil
262
+ # safe_constantize('UnknownModule') # => nil
263
+ # safe_constantize('UnknownModule::Foo::Bar') # => nil
264
+ def safe_constantize(camel_cased_word)
265
+ constantize(camel_cased_word)
266
+ rescue NameError => e
267
+ raise if e.name && !(camel_cased_word.to_s.split("::").include?(e.name.to_s) ||
268
+ e.name.to_s == camel_cased_word.to_s)
269
+ rescue ArgumentError => e
270
+ raise unless /not missing constant #{const_regexp(camel_cased_word)}!$/.match?(e.message)
271
+ end
272
+
273
+ # Returns the suffix that should be added to a number to denote the position
274
+ # in an ordered sequence such as 1st, 2nd, 3rd, 4th.
275
+ #
276
+ # ordinal(1) # => "st"
277
+ # ordinal(2) # => "nd"
278
+ # ordinal(1002) # => "nd"
279
+ # ordinal(1003) # => "rd"
280
+ # ordinal(-11) # => "th"
281
+ # ordinal(-1021) # => "st"
282
+ def ordinal(number)
283
+ abs_number = number.to_i.abs
284
+
285
+ if (11..13).include?(abs_number % 100)
286
+ "th"
287
+ else
288
+ case abs_number % 10
289
+ when 1; "st"
290
+ when 2; "nd"
291
+ when 3; "rd"
292
+ else "th"
293
+ end
294
+ end
295
+ end
296
+
297
+ # Turns a number into an ordinal string used to denote the position in an
298
+ # ordered sequence such as 1st, 2nd, 3rd, 4th.
299
+ #
300
+ # ordinalize(1) # => "1st"
301
+ # ordinalize(2) # => "2nd"
302
+ # ordinalize(1002) # => "1002nd"
303
+ # ordinalize(1003) # => "1003rd"
304
+ # ordinalize(-11) # => "-11th"
305
+ # ordinalize(-1021) # => "-1021st"
306
+ def ordinalize(number)
307
+ "#{number}#{ordinal(number)}"
308
+ end
309
+
310
+ private
311
+
312
+ # Mounts a regular expression, returned as a string to ease interpolation,
313
+ # that will match part by part the given constant.
314
+ #
315
+ # const_regexp("Foo::Bar::Baz") # => "Foo(::Bar(::Baz)?)?"
316
+ # const_regexp("::") # => "::"
317
+ def const_regexp(camel_cased_word)
318
+ parts = camel_cased_word.split("::".freeze)
319
+
320
+ return Regexp.escape(camel_cased_word) if parts.blank?
321
+
322
+ last = parts.pop
323
+
324
+ parts.reverse.inject(last) do |acc, part|
325
+ part.empty? ? acc : "#{part}(::#{acc})?"
326
+ end
327
+ end
328
+
329
+ # Applies inflection rules for +singularize+ and +pluralize+.
330
+ #
331
+ # If passed an optional +locale+ parameter, the uncountables will be
332
+ # found for that locale.
333
+ #
334
+ # apply_inflections('post', inflections.plurals, :en) # => "posts"
335
+ # apply_inflections('posts', inflections.singulars, :en) # => "post"
336
+ # def apply_inflections(word, rules, locale = :en)
337
+ # result = word.to_s.dup
338
+ #
339
+ # if word.empty? || inflections(locale).uncountables.uncountable?(result)
340
+ # result
341
+ # else
342
+ # rules.each { |(rule, replacement)| break if result.sub!(rule, replacement) }
343
+ # result
344
+ # end
345
+ # end
346
+ end
347
+ end
348
+ end
data/lib/fusu/try.rb ADDED
@@ -0,0 +1,20 @@
1
+ module Fusu
2
+ module Try
3
+ def try(object, *a, &b)
4
+ return nil if object === nil
5
+ try!(object, *a, &b) if a.empty? || object.respond_to?(a.first)
6
+ end
7
+
8
+ def try!(object, *a, &b)
9
+ if a.empty? && block_given?
10
+ if b.arity == 0
11
+ object.instance_eval(&b)
12
+ else
13
+ yield object
14
+ end
15
+ else
16
+ object.public_send(*a, &b)
17
+ end
18
+ end
19
+ end
20
+ end
data/lib/fusu/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Fusu
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fusu
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Artur Panyach
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2017-10-21 00:00:00.000000000 Z
11
+ date: 2017-11-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -73,6 +73,17 @@ files:
73
73
  - lib/fusu.rb
74
74
  - lib/fusu/array.rb
75
75
  - lib/fusu/blank.rb
76
+ - lib/fusu/concern.rb
77
+ - lib/fusu/hash.rb
78
+ - lib/fusu/hash/deep_merge.rb
79
+ - lib/fusu/hash/except.rb
80
+ - lib/fusu/hash/keys.rb
81
+ - lib/fusu/hash/reverse_merge.rb
82
+ - lib/fusu/hash_with_indifferent_access.rb
83
+ - lib/fusu/regexp.rb
84
+ - lib/fusu/string.rb
85
+ - lib/fusu/string/methods.rb
86
+ - lib/fusu/try.rb
76
87
  - lib/fusu/version.rb
77
88
  homepage: https://github.com/arturictus/fusu
78
89
  licenses: