sleeping_king_studios-tools 0.7.1 → 0.8.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (27) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +32 -3
  3. data/DEVELOPMENT.md +5 -7
  4. data/README.md +3 -64
  5. data/lib/sleeping_king_studios/tools.rb +12 -6
  6. data/lib/sleeping_king_studios/tools/all.rb +8 -3
  7. data/lib/sleeping_king_studios/tools/array_tools.rb +83 -58
  8. data/lib/sleeping_king_studios/tools/base.rb +18 -0
  9. data/lib/sleeping_king_studios/tools/core_tools.rb +68 -22
  10. data/lib/sleeping_king_studios/tools/enumerable_tools.rb +6 -3
  11. data/lib/sleeping_king_studios/tools/hash_tools.rb +59 -47
  12. data/lib/sleeping_king_studios/tools/integer_tools.rb +97 -55
  13. data/lib/sleeping_king_studios/tools/object_tools.rb +67 -50
  14. data/lib/sleeping_king_studios/tools/string_tools.rb +81 -63
  15. data/lib/sleeping_king_studios/tools/toolbelt.rb +46 -22
  16. data/lib/sleeping_king_studios/tools/toolbox.rb +2 -2
  17. data/lib/sleeping_king_studios/tools/toolbox/configuration.rb +197 -122
  18. data/lib/sleeping_king_studios/tools/toolbox/constant_map.rb +24 -51
  19. data/lib/sleeping_king_studios/tools/toolbox/delegator.rb +50 -29
  20. data/lib/sleeping_king_studios/tools/toolbox/inflector.rb +130 -0
  21. data/lib/sleeping_king_studios/tools/toolbox/inflector/rules.rb +171 -0
  22. data/lib/sleeping_king_studios/tools/toolbox/mixin.rb +10 -10
  23. data/lib/sleeping_king_studios/tools/toolbox/semantic_version.rb +15 -14
  24. data/lib/sleeping_king_studios/tools/version.rb +6 -8
  25. metadata +84 -26
  26. data/lib/sleeping_king_studios/tools/semantic_version.rb +0 -15
  27. data/lib/sleeping_king_studios/tools/string_tools/plural_inflector.rb +0 -185
@@ -1,26 +1,21 @@
1
- # lib/sleeping_king_studios/tools/toolbox/constant_map.rb
1
+ # frozen_string_literal: true
2
2
 
3
3
  require 'sleeping_king_studios/tools/string_tools'
4
4
  require 'sleeping_king_studios/tools/toolbox'
5
5
 
6
6
  module SleepingKingStudios::Tools::Toolbox
7
7
  # Provides an enumerable interface for defining a group of constants.
8
- module ConstantMap
9
- class << self
10
- # Creates a new ConstantMap.
11
- #
12
- # @param constants [Hash] The constants to define.
13
- def new constants
14
- mod = Module.new
15
- mod.extend self
8
+ class ConstantMap < Module
9
+ # @param constants [Hash] The constants to define.
10
+ def initialize(constants)
11
+ super()
16
12
 
17
- constants.each do |const_name, const_value|
18
- mod.const_set const_name, const_value
19
- end # each
13
+ constants.each do |const_name, const_value|
14
+ const_set(const_name, const_value)
20
15
 
21
- mod
22
- end # class method new
23
- end # eigenclass
16
+ define_reader(const_name)
17
+ end
18
+ end
24
19
 
25
20
  # Returns a hash with the names and values of the defined constants.
26
21
  #
@@ -28,17 +23,17 @@ module SleepingKingStudios::Tools::Toolbox
28
23
  def all
29
24
  constants.each.with_object({}) do |const_name, hsh|
30
25
  hsh[const_name] = const_get(const_name)
31
- end # each
32
- end # method all
26
+ end
27
+ end
33
28
 
34
29
  # Iterates through the defined constants, yielding the name and value of
35
30
  # each constant to the block.
36
31
  #
37
32
  # @yieldparam key [Symbol] The name of the symbol.
38
33
  # @yieldparam value [Object] The value of the symbol.
39
- def each &block
34
+ def each(&block)
40
35
  all.each(&block)
41
- end # method each
36
+ end
42
37
 
43
38
  # Freezes the constant map and recursively freezes every constant value
44
39
  # using ObjectTools#deep_freeze. Also pre-emptively defines any reader
@@ -47,48 +42,26 @@ module SleepingKingStudios::Tools::Toolbox
47
42
  # @see ObjectTools#deep_freeze
48
43
  def freeze
49
44
  constants.each do |const_name|
50
- reader_name = const_name.downcase
51
-
52
- define_reader(const_name, reader_name) unless methods.include?(reader_name)
53
-
54
45
  object_tools.deep_freeze const_get(const_name)
55
- end # each
46
+ end
56
47
 
57
48
  super
58
- end # method freeze
49
+ end
59
50
 
60
51
  private
61
52
 
62
- def define_reader const_name, reader_name = nil
63
- reader_name ||= const_name.downcase
64
-
65
- define_singleton_method reader_name, ->() { const_get const_name }
66
- end # method define_reader
67
-
68
- def method_missing symbol, *args, &block
69
- const_name = string_tools.underscore(symbol.to_s).upcase.intern
70
-
71
- if constants.include?(const_name)
72
- define_reader(const_name, symbol)
53
+ def define_reader(const_name, reader_name = nil)
54
+ reader_name ||= string_tools.underscore(const_name.to_s).intern
73
55
 
74
- return send(symbol, *args, &block)
75
- end # if
76
-
77
- super
78
- end # method method_missing
56
+ define_singleton_method(reader_name) { const_get const_name }
57
+ end
79
58
 
80
59
  def object_tools
81
60
  ::SleepingKingStudios::Tools::ObjectTools
82
- end # method object_tools
83
-
84
- def respond_to_missing? symbol, include_all = false
85
- const_name = string_tools.underscore(symbol.to_s).upcase.intern
86
-
87
- constants.include?(const_name) || super
88
- end # method respond_to_missing?
61
+ end
89
62
 
90
63
  def string_tools
91
64
  ::SleepingKingStudios::Tools::StringTools
92
- end # method string_tools
93
- end # module
94
- end # module
65
+ end
66
+ end
67
+ end
@@ -1,7 +1,12 @@
1
- # lib/sleeping_king_studios/tools/toolbox/delegator.rb
1
+ # frozen_string_literal: true
2
2
 
3
+ require 'sleeping_king_studios/tools/core_tools'
3
4
  require 'sleeping_king_studios/tools/toolbox'
4
5
 
6
+ # rubocop:disable Metrics/AbcSize
7
+ # rubocop:disable Metrics/CyclomaticComplexity
8
+ # rubocop:disable Metrics/MethodLength
9
+ # rubocop:disable Metrics/PerceivedComplexity
5
10
  module SleepingKingStudios::Tools::Toolbox
6
11
  # Module for extending classes with basic delegation. Supports passing
7
12
  # arguments, keywords, and blocks to the delegated method.
@@ -13,6 +18,15 @@ module SleepingKingStudios::Tools::Toolbox
13
18
  # delegate :my_method, :to => MyService
14
19
  # end # class
15
20
  module Delegator
21
+ def self.extended(_module)
22
+ super
23
+
24
+ SleepingKingStudios::Tools::CoreTools.deprecate(
25
+ 'SleepingKingStudios::Tools::Toolbox::Delegator',
26
+ message: 'Use Ruby stdlib Forwardable instead.'
27
+ )
28
+ end
29
+
16
30
  # Defines a wrapper method to delegate implementation of the specified
17
31
  # method or methods to an object, to the object at another specified method,
18
32
  # or to the object at a specified instance variable.
@@ -58,13 +72,13 @@ module SleepingKingStudios::Tools::Toolbox
58
72
  # variable of that name and call `method_name` on the result.
59
73
  #
60
74
  # @raise ArgumentError if no delegate is specified.
61
- def delegate *method_names, to: nil, allow_nil: false
62
- raise ArgumentError.new('must specify a delegate') if to.nil? && !allow_nil
75
+ def delegate(*method_names, to: nil, allow_nil: false)
76
+ raise ArgumentError, 'must specify a delegate' if to.nil? && !allow_nil
63
77
 
64
78
  method_names.each do |method_name|
65
- delegate_method method_name, to, { :allow_nil => !!allow_nil }
66
- end # each
67
- end # method delegate
79
+ delegate_method method_name, to, { allow_nil: !!allow_nil }
80
+ end
81
+ end
68
82
 
69
83
  # Wraps a delegate object by automatically delegating each method that is
70
84
  # defined on the delegate class from the instance to the delegate. The
@@ -79,7 +93,10 @@ module SleepingKingStudios::Tools::Toolbox
79
93
  # class Errors
80
94
  # extend SleepingKingStudios::Tools::Delegator
81
95
  #
82
- # wrap_delegate Hash.new { |hsh, key| hsh[key] = Errors.new }, :klass => Hash
96
+ # wrap_delegate(
97
+ # Hash.new { |hsh, key| hsh[key] = Errors.new },
98
+ # :klass => Hash
99
+ # )
83
100
  #
84
101
  # def messages
85
102
  # @messages ||= []
@@ -116,33 +133,33 @@ module SleepingKingStudios::Tools::Toolbox
116
133
  # belong to the specified klass.
117
134
  #
118
135
  # @see #delegate
119
- def wrap_delegate target, klass: nil, except: [], only: []
136
+ def wrap_delegate(target, klass: nil, except: [], only: [])
120
137
  if klass.is_a?(Module)
121
- unless target.is_a?(String) || target.is_a?(Symbol) || target.is_a?(klass)
122
- raise ArgumentError.new "expected delegate to be a #{klass.name}"
123
- end # unless
138
+ unless target.is_a?(String) ||
139
+ target.is_a?(Symbol) ||
140
+ target.is_a?(klass)
141
+ raise ArgumentError, "expected delegate to be a #{klass.name}"
142
+ end
124
143
 
125
144
  method_names = klass.instance_methods - Object.instance_methods
126
145
  elsif target.is_a?(String) || target.is_a?(Symbol)
127
- raise ArgumentError.new 'must specify a delegate class'
146
+ raise ArgumentError, 'must specify a delegate class'
128
147
  else
129
148
  method_names = target.methods - Object.new.methods
130
- end # if-elsif-else
149
+ end
131
150
 
132
151
  if except.is_a?(Array) && !except.empty?
133
- method_names = method_names - except.map(&:intern)
134
- end # if
152
+ method_names -= except.map(&:intern)
153
+ end
135
154
 
136
- if only.is_a?(Array) && !only.empty?
137
- method_names = method_names & only.map(&:intern)
138
- end # if
155
+ method_names &= only.map(&:intern) if only.is_a?(Array) && !only.empty?
139
156
 
140
- delegate *method_names, :to => target
141
- end # method wrap_delegate
157
+ delegate(*method_names, to: target)
158
+ end
142
159
 
143
160
  private
144
161
 
145
- def delegate_method method_name, target, options = {}
162
+ def delegate_method(method_name, target, options = {})
146
163
  if target.is_a?(String) || target.is_a?(Symbol)
147
164
  target = target.intern
148
165
 
@@ -153,7 +170,7 @@ module SleepingKingStudios::Tools::Toolbox
153
170
  return nil if receiver.nil? && options[:allow_nil]
154
171
 
155
172
  receiver.send(method_name, *args, &block)
156
- end # define_method
173
+ end
157
174
  else
158
175
  define_method method_name do |*args, &block|
159
176
  receiver = send(target)
@@ -161,15 +178,19 @@ module SleepingKingStudios::Tools::Toolbox
161
178
  return nil if receiver.nil? && options[:allow_nil]
162
179
 
163
180
  receiver.send(method_name, *args, &block)
164
- end # define_method
165
- end # if-else
181
+ end
182
+ end
166
183
  else
167
184
  define_method method_name do |*args, &block|
168
185
  return nil if target.nil? && options[:allow_nil]
169
186
 
170
187
  target.send(method_name, *args, &block)
171
- end # define_method
172
- end # if
173
- end # method delegate_method
174
- end # module
175
- end # module
188
+ end
189
+ end
190
+ end
191
+ end
192
+ end
193
+ # rubocop:enable Metrics/AbcSize
194
+ # rubocop:enable Metrics/CyclomaticComplexity
195
+ # rubocop:enable Metrics/MethodLength
196
+ # rubocop:enable Metrics/PerceivedComplexity
@@ -0,0 +1,130 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'forwardable'
4
+
5
+ require 'sleeping_king_studios/tools/toolbox'
6
+
7
+ module SleepingKingStudios::Tools::Toolbox
8
+ # Transforms words (e.g. from singular to plural).
9
+ #
10
+ # Should maintain the same interface as ActiveSupport::Inflector.
11
+ class Inflector
12
+ extend Forwardable
13
+
14
+ autoload :Rules, 'sleeping_king_studios/tools/toolbox/inflector/rules'
15
+
16
+ def_delegators :@rules,
17
+ :irregular_words,
18
+ :irregular_words_reversed,
19
+ :plural_rules,
20
+ :singular_rules,
21
+ :uncountable_words
22
+
23
+ private \
24
+ :irregular_words,
25
+ :irregular_words_reversed,
26
+ :plural_rules,
27
+ :singular_rules,
28
+ :uncountable_words
29
+
30
+ # @return [Rules] An object defining the transformation rules.
31
+ def initialize(rules: nil)
32
+ @rules = rules || Rules.new
33
+ end
34
+
35
+ # @return [Rules] The defined rules object for the inflector.
36
+ attr_reader :rules
37
+
38
+ # Transforms the word to CamelCase.
39
+ #
40
+ # @param word [String] The word to transform.
41
+ # @param uppercase_first_letter [Boolean] If true, the first letter is
42
+ # capitalized. Defaults to true.
43
+ #
44
+ # @return [String] The word in CamelCase.
45
+ def camelize(word, uppercase_first_letter = true)
46
+ return '' if word.nil? || word.empty?
47
+
48
+ word = word.to_s.gsub(/(\b|[_-])([a-z])/) { Regexp.last_match(2).upcase }
49
+
50
+ (uppercase_first_letter ? word[0].upcase : word[0].downcase) + word[1..-1]
51
+ end
52
+
53
+ # rubocop:disable Metrics/AbcSize
54
+ # rubocop:disable Metrics/CyclomaticComplexity
55
+
56
+ # Transforms the word to a plural, lowercase form.
57
+ #
58
+ # @param word [String] The word to transform.
59
+ #
60
+ # @return [String] The word in plural form.
61
+ def pluralize(word)
62
+ return '' if word.nil? || word.empty?
63
+
64
+ normalized = word.to_s.strip.downcase
65
+
66
+ return normalized if uncountable_words.include?(normalized)
67
+
68
+ return normalized if irregular_words_reversed.key?(normalized)
69
+
70
+ return irregular_words[normalized] if irregular_words.key?(normalized)
71
+
72
+ plural_rules.each do |match, replace|
73
+ next unless normalized =~ match
74
+
75
+ return normalized.sub(match, replace)
76
+ end
77
+
78
+ word
79
+ end
80
+ # rubocop:enable Metrics/AbcSize
81
+ # rubocop:enable Metrics/CyclomaticComplexity
82
+
83
+ # rubocop:disable Metrics/AbcSize
84
+ # rubocop:disable Metrics/MethodLength
85
+
86
+ # Transforms the word to a singular, lowercase form.
87
+ #
88
+ # @param word [String] The word to transform.
89
+ #
90
+ # @return [String] The word in singular form.
91
+ def singularize(word)
92
+ return '' if word.nil? || word.empty?
93
+
94
+ normalized = word.to_s.strip.downcase
95
+
96
+ return normalized if irregular_words.key?(normalized)
97
+
98
+ if irregular_words_reversed.key?(normalized)
99
+ return irregular_words_reversed[normalized]
100
+ end
101
+
102
+ singular_rules.each do |match, replace|
103
+ next unless normalized =~ match
104
+
105
+ return normalized.sub(match, replace)
106
+ end
107
+
108
+ word
109
+ end
110
+
111
+ # rubocop:enable Metrics/AbcSize
112
+ # rubocop:enable Metrics/MethodLength
113
+
114
+ # Transforms the word to a lowercase, underscore-separated form.
115
+ #
116
+ # @params word [String] the word to transform.
117
+ #
118
+ # @return [String] The word in underscored form.
119
+ def underscore(word)
120
+ return '' if word.nil? || word.empty?
121
+
122
+ word = word.to_s.gsub(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
123
+
124
+ word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
125
+ word.tr!('-', '_')
126
+ word.downcase!
127
+ word
128
+ end
129
+ end
130
+ end
@@ -0,0 +1,171 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sleeping_king_studios/tools/toolbox'
4
+
5
+ class SleepingKingStudios::Tools::Toolbox::Inflector
6
+ # Rules for inflecting words.
7
+ class Rules
8
+ # @param irregular_words [Hash<String, String>] Hash of irregular word
9
+ # pairs in singular => plural order, e.g. "child" => "children".
10
+ # @param plural_rules [Array<Array<(Regexp, String)>>] Rules for
11
+ # pluralizing words.
12
+ # @param singular_rules [Array<Array<(Regexp, String)>>] Rules for
13
+ # singularizing words.
14
+ # @param uncountable_words [Array<String>] List of uncountable words,
15
+ # e.g. "data".
16
+ def initialize(
17
+ irregular_words: nil,
18
+ plural_rules: nil,
19
+ singular_rules: nil,
20
+ uncountable_words: nil
21
+ )
22
+ @plural_rules = plural_rules || default_plural_rules
23
+ @singular_rules = singular_rules || default_singular_rules
24
+ @irregular_words = irregular_words || default_irregular_words
25
+ @uncountable_words =
26
+ Set.new(uncountable_words || default_uncountable_words)
27
+
28
+ @irregular_words_reversed = reverse_hash(@irregular_words)
29
+ end
30
+
31
+ # @return [Array<Array<(String, String)>] Hash of irregular word pairs in
32
+ # singular => plural order.
33
+ attr_reader :irregular_words
34
+
35
+ # @return [Array<Array<(String, String)>] Hash of irregular word pairs in
36
+ # plural => singular order.
37
+ attr_reader :irregular_words_reversed
38
+
39
+ # @return [Array<Array<(Regexp, String)>>] Rules for pluralizing words.
40
+ attr_reader :plural_rules
41
+
42
+ # @return [Array<Array<(Regexp, String)>>] Rules for singularizing words.
43
+ attr_reader :singular_rules
44
+
45
+ # @return [Array<String>] List of uncountable words.
46
+ attr_reader :uncountable_words
47
+
48
+ # Defines an irregular word pair.
49
+ #
50
+ # @param singular [String] The singular form of the word.
51
+ # @param plural [String] The plural form of the word.
52
+ #
53
+ # @return [Rules] The rules object.
54
+ def define_irregular_word(singular, plural)
55
+ validate_string(singular)
56
+ validate_string(plural)
57
+
58
+ @irregular_words[singular] = plural
59
+ @irregular_words_reversed[plural] = singular
60
+
61
+ self
62
+ end
63
+
64
+ # Defines a pluralization rule.
65
+ #
66
+ # @param pattern [Regexp] The pattern to match.
67
+ # @param replace [String] The string to replace.
68
+ #
69
+ # @return [Rules] The rules object.
70
+ def define_plural_rule(pattern, replace)
71
+ validate_pattern(pattern)
72
+ validate_string(replace, as: 'replace')
73
+
74
+ @plural_rules.unshift([pattern, replace])
75
+
76
+ self
77
+ end
78
+
79
+ # Defines a singularization rule.
80
+ #
81
+ # @param pattern [Regexp] The pattern to match.
82
+ # @param replace [String] The string to replace.
83
+ #
84
+ # @return [Rules] The rules object.
85
+ def define_singular_rule(pattern, replace)
86
+ validate_pattern(pattern)
87
+ validate_string(replace, as: 'replace')
88
+
89
+ @singular_rules.unshift([pattern, replace])
90
+
91
+ self
92
+ end
93
+
94
+ # Defines an uncountable word.
95
+ #
96
+ # @param word [String] The uncountable word.
97
+ #
98
+ # @return [Rules] The rules object.
99
+ def define_uncountable_word(word)
100
+ validate_string(word)
101
+
102
+ @uncountable_words << word
103
+
104
+ self
105
+ end
106
+
107
+ # @return [String] A human-readable representation of the rules object.
108
+ def inspect
109
+ "#<SleepingKingStudios::Tools::Toolbox::Inflector::Rules:#{object_id}>"
110
+ end
111
+
112
+ private
113
+
114
+ def default_irregular_words
115
+ {
116
+ 'child' => 'children',
117
+ 'person' => 'people'
118
+ }
119
+ end
120
+
121
+ def default_plural_rules
122
+ [
123
+ [/([^aeiouy])y$/i, '\1ies'], # Winery => Wineries
124
+ [/([^aeiouy]o)$/i, '\1es'], # Halo => Haloes
125
+ [/(ss|[xz]|[cs]h)$/i, '\1es'], # Truss => Trusses
126
+ [/s$/i, 's'], # Words => Words
127
+ [/$/, 's'] # Word => Words
128
+ ]
129
+ end
130
+
131
+ def default_singular_rules
132
+ [
133
+ [/([^aeiouy])ies$/i, '\1y'], # Wineries => Winery
134
+ [/([^aeiouy]o)es$/, '\1'], # Haloes => Halo
135
+ [/(ss|[sxz]|[cs]h)es$/, '\1'], # Torches => Torch
136
+ [/ss$/i, 'ss'], # Truss => Truss
137
+ [/s$/i, ''] # Words => Word
138
+ ]
139
+ end
140
+
141
+ def default_uncountable_words
142
+ %w[data]
143
+ end
144
+
145
+ def reverse_hash(hsh)
146
+ hsh.each.with_object({}) do |(key, value), reversed|
147
+ reversed[value] = key
148
+ end
149
+ end
150
+
151
+ def validate_pattern(rxp)
152
+ raise ArgumentError, "pattern can't be blank", caller(1..-1) if rxp.nil?
153
+
154
+ return if rxp.is_a?(Regexp)
155
+
156
+ raise ArgumentError, 'pattern must be a Regexp', caller(1..-1)
157
+ end
158
+
159
+ def validate_string(word, as: 'word')
160
+ raise ArgumentError, "#{as} can't be blank", caller(1..-1) if word.nil?
161
+
162
+ unless word.is_a?(String)
163
+ raise ArgumentError, "#{as} must be a String", caller(1..-1)
164
+ end
165
+
166
+ return unless word.empty?
167
+
168
+ raise ArgumentError, "#{as} can't be blank", caller(1..-1)
169
+ end
170
+ end
171
+ end