sleeping_king_studios-tools 0.7.1 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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