dry-inflector 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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 91485a1eac7b8b76f5a017b6eb04d567b54ae0685fd30ba1e3378450343de3f4
4
+ data.tar.gz: ee1e2b939347eda2577ddff9d3d454cc1e3115d2ac298260ea6215d26d5b78ec
5
+ SHA512:
6
+ metadata.gz: 3d6f9a7fb8249866c30008a11b93715040e5b24106e83e731be32e15dc10c5e8b06d90072d21670e33b44dec41a5ceb075f076dc038d1dfb6424e6185a4ee283
7
+ data.tar.gz: e238694af29c1611b54630b9aecfa8b8c1b1096596393f811a9a8430a82d83f71e39e27b72337f504870f4122559ceb8cea7c9a4ffa05b8310dc616b7fa21282
@@ -0,0 +1,52 @@
1
+ # dry-inflector
2
+
3
+ Inflector for Ruby
4
+
5
+ ## v0.2.0 - 2019-10-13
6
+
7
+ ### Added
8
+
9
+ - [Abinoam P. Marques Jr. & Andrii Savchenko] Introduced `Dry::Inflector#upper_camelize` and `Dry::Inflector#lower_camelize`. `Dry::Inflector#camelize` is now an alias for `Dry::Inflector#upper_camelize` and may be deprecated later.
10
+ ```ruby
11
+ inflector.camelize_upper("data_mapper") # => "DataMapper"
12
+ inflector.camelize_lower("data_mapper") # => "dataMapper"
13
+ ```
14
+
15
+ ### Fixed
16
+
17
+ - [ecnal] Fixed singularization rules for words like "alias" or "status"
18
+
19
+ [Compare v0.1.2...v0.2.0](https://github.com/dry-rb/dry-inflector/compare/v0.1.2...v0.2.0)
20
+
21
+ ## v0.1.2 - 2018-04-25
22
+
23
+ ### Added
24
+
25
+ - [Gustavo Caso & Nikita Shilnikov] Added support for acronyms
26
+
27
+ [Compare v0.1.1...v0.1.2](https://github.com/dry-rb/dry-inflector/compare/v0.1.1...v0.1.2)
28
+
29
+ ## v0.1.1 - 2017-11-18
30
+
31
+ ### Fixed
32
+
33
+ - [Luca Guidi & Abinoam P. Marques Jr.] Ensure `Dry::Inflector#ordinalize` to work for all the numbers from 0 to 100
34
+
35
+ [Compare v0.1.0...v0.1.1](https://github.com/dry-rb/dry-inflector/compare/v0.1.0...v0.1.1)
36
+
37
+ ## v0.1.0 - 2017-11-17
38
+
39
+ ### Added
40
+
41
+ - [Luca Guidi] Introduced `Dry::Inflector#pluralize`
42
+ - [Luca Guidi] Introduced `Dry::Inflector#singularize`
43
+ - [Luca Guidi] Introduced `Dry::Inflector#camelize`
44
+ - [Luca Guidi] Introduced `Dry::Inflector#classify`
45
+ - [Luca Guidi] Introduced `Dry::Inflector#tableize`
46
+ - [Luca Guidi] Introduced `Dry::Inflector#dasherize`
47
+ - [Luca Guidi] Introduced `Dry::Inflector#underscore`
48
+ - [Luca Guidi] Introduced `Dry::Inflector#demodulize`
49
+ - [Luca Guidi] Introduced `Dry::Inflector#humanize`
50
+ - [Luca Guidi] Introduced `Dry::Inflector#ordinalize`
51
+ - [Abinoam P. Marques Jr.] Introduced `Dry::Inflector#foreign_key`
52
+ - [Abinoam P. Marques Jr.] Introduced `Dry::Inflector#constantize`
@@ -0,0 +1,22 @@
1
+ Copyright © The Dry, Rails, Merb, Datamapper, Inflecto, Flexus, and Hanami teams
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,106 @@
1
+ [gem]: https://img.shields.io/gem/v/dry-inflector.svg
2
+ [travis]: https://travis-ci.org/dry-rb/dry-inflector
3
+ [codeclimate]: https://codeclimate.com/github/dry-rb/dry-inflector
4
+ [chat]: https://dry-rb.zulipchat.com
5
+ [inchpages]: http://inch-ci.org/github/dry-rb/dry-inflector
6
+
7
+ # dry-inflector [![Join the chat at https://dry-rb.zulipchat.com](https://img.shields.io/badge/dry--rb-join%20chat-%23346b7a.svg)][chat]
8
+
9
+ [![Gem Version](https://badge.fury.io/rb/dry-inflector.svg)][gem]
10
+ [![Build Status](https://travis-ci.org/dry-rb/dry-inflector.svg?branch=master)][travis]
11
+ [![Code Climate](https://codeclimate.com/github/dry-rb/dry-inflector/badges/gpa.svg)][codeclimate]
12
+ [![Test Coverage](https://codeclimate.com/github/dry-rb/dry-inflector/badges/coverage.svg)][codeclimate]
13
+ [![Inline docs](http://inch-ci.org/github/dry-rb/dry-inflector.svg?branch=master)][inchpages]
14
+
15
+ dry-inflector is an inflector gem for Ruby.
16
+
17
+ ## Installation
18
+
19
+ Add this line to your application's `Gemfile`:
20
+
21
+ ```ruby
22
+ gem 'dry-inflector'
23
+ ```
24
+
25
+ And then execute:
26
+
27
+ ```shell
28
+ $ bundle
29
+ ```
30
+
31
+ Or install it yourself as:
32
+
33
+ ```shell
34
+ $ gem install dry-inflector
35
+ ```
36
+
37
+ ## Usage
38
+
39
+ ### Basic usage
40
+
41
+ ```ruby
42
+ require "dry/inflector"
43
+
44
+ inflector = Dry::Inflector.new
45
+
46
+ inflector.pluralize("book") # => "books"
47
+ inflector.singularize("books") # => "book"
48
+
49
+ inflector.camelize("dry/inflector") # => "Dry::Inflector"
50
+ inflector.classify("books") # => "Book"
51
+ inflector.tableize("Book") # => "books"
52
+
53
+ inflector.dasherize("dry_inflector") # => "dry-inflector"
54
+ inflector.underscore("dry-inflector") # => "dry_inflector"
55
+
56
+ inflector.demodulize("Dry::Inflector") # => "Inflector"
57
+
58
+ inflector.humanize("dry_inflector") # => "Dry inflector"
59
+ inflector.humanize("author_id") # => "Author"
60
+
61
+ inflector.ordinalize(1) # => "1st"
62
+ inflector.ordinalize(2) # => "2nd"
63
+ inflector.ordinalize(3) # => "3rd"
64
+ inflector.ordinalize(10) # => "10th"
65
+ inflector.ordinalize(23) # => "23rd"
66
+ ```
67
+
68
+ ### Custom inflection rules
69
+
70
+ ```ruby
71
+ require "dry/inflector"
72
+
73
+ inflector = Dry::Inflector.new do |inflections|
74
+ inflections.plural "virus", "viruses" # specify a rule for #pluralize
75
+ inflections.singular "thieves", "thief" # specify a rule for #singularize
76
+ inflections.uncountable "dry-inflector" # add an exception for an uncountable word
77
+ end
78
+
79
+ inflector.pluralize("virus") # => "viruses"
80
+ inflector.singularize("thieves") # => "thief"
81
+
82
+ inflector.pluralize("dry-inflector") # => "dry-inflector"
83
+ ```
84
+
85
+ ## Credits
86
+
87
+ This gem is the cumulative effort of the Ruby community.
88
+ It started with the extlib inflecto originated from [active_support](https://github.com/rails/rails), then dm-core inflector originated from [extlib](https://github.com/datamapper/extlib).
89
+ Later, [`inflecto`](https://github.com/mbj/inflecto) was extracted from [dm-core](https://github.com/datamapper/dm-core) as a standalone inflector.
90
+ Now, we resurrect `inflecto` and merged [`flexus`](https://github.com/Ptico/flexus), with some inflection rules from [`hanami-utils`](https://github.com/hanami/utils).
91
+
92
+ This is `dry-inflector`.
93
+
94
+ ## Development
95
+
96
+ After checking out the repo, run `bin/setup` to install dependencies. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
97
+
98
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
99
+
100
+ ## Contributing
101
+
102
+ Bug reports and pull requests are welcome on GitHub at https://github.com/dry-rb/dry-inflector.
103
+
104
+ ## Copyright
105
+
106
+ Copyright © The Dry, Rails, Merb, Datamapper, Inflecto, Flexus, and Hanami teams - Released under the MIT License
@@ -0,0 +1,36 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ lib = File.expand_path("../lib", __FILE__)
5
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
6
+ require "dry/inflector/version"
7
+
8
+ Gem::Specification.new do |spec|
9
+ spec.name = "dry-inflector"
10
+ spec.version = Dry::Inflector::VERSION
11
+ spec.authors = ["Luca Guidi", "Andrii Savchenko", "Abinoam P. Marques Jr."]
12
+ spec.email = ["me@lucaguidi.com", "andrey@aejis.eu", "abinoam@gmail.com"]
13
+
14
+ spec.summary = "DRY Inflector"
15
+ spec.description = "String inflections for dry-rb"
16
+ spec.homepage = "https://dry-rb.org"
17
+ spec.license = "MIT"
18
+
19
+ spec.metadata["allowed_push_host"] = "https://rubygems.org"
20
+ spec.metadata["changelog_uri"] = "https://github.com/dry-rb/dry-inflector/blob/master/CHANGELOG.md"
21
+ spec.metadata["source_code_uri"] = "https://github.com/dry-rb/dry-inflector"
22
+ spec.metadata["bug_tracker_uri"] = "https://github.com/dry-rb/dry-inflector/issues"
23
+
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.files = `git ls-files -- lib/* CHANGELOG.md LICENSE.md README.md dry-inflector.gemspec`.split($INPUT_RECORD_SEPARATOR)
29
+ spec.test_files = spec.files.grep(%r{^(test|spec|features)/})
30
+ spec.required_ruby_version = '>= 2.4'
31
+
32
+ spec.add_development_dependency "bundler"
33
+ spec.add_development_dependency "rake", "~> 12.0"
34
+ spec.add_development_dependency "rspec", "~> 3.7"
35
+ spec.add_development_dependency "rubocop", "~> 0.50.0"
36
+ end
@@ -0,0 +1 @@
1
+ require 'dry/inflector'
@@ -0,0 +1,339 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ # dry-inflector
5
+ #
6
+ # @since 0.1.0
7
+ class Inflector
8
+ require "dry/inflector/version"
9
+ require "dry/inflector/inflections"
10
+
11
+ # Instantiate the inflector
12
+ #
13
+ # @param blk [Proc] an optional block to specify custom inflection rules
14
+ # @yieldparam [Dry::Inflector::Inflections] the inflection rules
15
+ #
16
+ # @return [Dry::Inflector] the inflector
17
+ #
18
+ # @since 0.1.0
19
+ #
20
+ # @example Basic usage
21
+ # require "dry/inflector"
22
+ #
23
+ # inflector = Dry::Inflector.new
24
+ #
25
+ # @example Custom inflection rules
26
+ # require "dry/inflector"
27
+ #
28
+ # inflector = Dry::Inflector.new do |inflections|
29
+ # inflections.plural "virus", "viruses" # specify a rule for #pluralize
30
+ # inflections.singular "thieves", "thief" # specify a rule for #singularize
31
+ # inflections.uncountable "dry-inflector" # add an exception for an uncountable word
32
+ # end
33
+ def initialize(&blk)
34
+ @inflections = Inflections.build(&blk)
35
+ end
36
+
37
+ # Lower camelize a string
38
+ #
39
+ # @param input [String,Symbol] the input
40
+ # @return [String] the lower camelized string
41
+ #
42
+ # @since 0.1.3
43
+ #
44
+ # @example
45
+ # require "dry/inflector"
46
+ #
47
+ # inflector = Dry::Inflector.new
48
+ # inflector.camelize_lower("data_mapper") # => "dataMapper"
49
+ def camelize_lower(input)
50
+ internal_camelize(input, false)
51
+ end
52
+
53
+ # Upper camelize a string
54
+ #
55
+ # @param input [String,Symbol] the input
56
+ # @return [String] the upper camelized string
57
+ #
58
+ # @since 0.1.3
59
+ #
60
+ # @example
61
+ # require "dry/inflector"
62
+ #
63
+ # inflector = Dry::Inflector.new
64
+ # inflector.camelize_upper("data_mapper") # => "DataMapper"
65
+ # inflector.camelize_upper("dry/inflector") # => "Dry::Inflector"
66
+ def camelize_upper(input)
67
+ internal_camelize(input, true)
68
+ end
69
+
70
+ alias :camelize :camelize_upper
71
+
72
+
73
+ # Find a constant with the name specified in the argument string
74
+ #
75
+ # The name is assumed to be the one of a top-level constant,
76
+ # constant scope of caller is ignored
77
+ #
78
+ # @param input [String,Symbol] the input
79
+ # @return [Class, Module] the class or module
80
+ #
81
+ # @since 0.1.0
82
+ #
83
+ # @example
84
+ # require "dry/inflector"
85
+ #
86
+ # inflector = Dry::Inflector.new
87
+ # inflector.constantize("Module") # => Module
88
+ # inflector.constantize("Dry::Inflector") # => Dry::Inflector
89
+ def constantize(input)
90
+ Object.const_get(input)
91
+ end
92
+
93
+ # Classify a string
94
+ #
95
+ # @param input [String,Symbol] the input
96
+ # @return [String] the classified string
97
+ #
98
+ # @since 0.1.0
99
+ #
100
+ # @example
101
+ # require "dry/inflector"
102
+ #
103
+ # inflector = Dry::Inflector.new
104
+ # inflector.classify("books") # => "Book"
105
+ def classify(input)
106
+ camelize(singularize(input.to_s.sub(/.*\./, "")))
107
+ end
108
+
109
+ # Dasherize a string
110
+ #
111
+ # @param input [String,Symbol] the input
112
+ # @return [String] the dasherized string
113
+ #
114
+ # @since 0.1.0
115
+ #
116
+ # @example
117
+ # require "dry/inflector"
118
+ #
119
+ # inflector = Dry::Inflector.new
120
+ # inflector.dasherize("dry_inflector") # => "dry-inflector"
121
+ def dasherize(input)
122
+ input.to_s.tr("_", "-")
123
+ end
124
+
125
+ # Demodulize a string
126
+ #
127
+ # @param input [String,Symbol] the input
128
+ # @return [String] the demodulized string
129
+ #
130
+ # @since 0.1.0
131
+ #
132
+ # @example
133
+ # require "dry/inflector"
134
+ #
135
+ # inflector = Dry::Inflector.new
136
+ # inflector.demodulize("Dry::Inflector") # => "Inflector"
137
+ def demodulize(input)
138
+ input.to_s.split("::").last
139
+ end
140
+
141
+ # Humanize a string
142
+ #
143
+ # @param input [String,Symbol] the input
144
+ # @return [String] the humanized string
145
+ #
146
+ # @since 0.1.0
147
+ #
148
+ # @example
149
+ # require "dry/inflector"
150
+ #
151
+ # inflector = Dry::Inflector.new
152
+ # inflector.humanize("dry_inflector") # => "Dry inflector"
153
+ # inflector.humanize("author_id") # => "Author"
154
+ def humanize(input)
155
+ input = input.to_s
156
+ result = inflections.humans.apply_to(input)
157
+ result.chomp!("_id")
158
+ result.tr!("_", " ")
159
+ match = /(?<separator>\W)/.match(result)
160
+ separator = match ? match[:separator] : DEFAULT_SEPARATOR
161
+ result.split(separator).map.with_index { |word, index|
162
+ inflections.acronyms.apply_to(word, index.zero?)
163
+ }.join(separator)
164
+ end
165
+
166
+ # Creates a foreign key name
167
+ #
168
+ # @param input [String, Symbol] the input
169
+ # @return [String] foreign key
170
+ #
171
+ # @example
172
+ # require "dry/inflector"
173
+ #
174
+ # inflector = Dry::Inflector.new
175
+ # inflector.foreign_key("Message") => "message_id"
176
+ def foreign_key(input)
177
+ "#{underscore(demodulize(input))}_id"
178
+ end
179
+
180
+ # Ordinalize a number
181
+ #
182
+ # @param number [Integer] the input
183
+ # @return [String] the ordinalized number
184
+ #
185
+ # @since 0.1.0
186
+ #
187
+ # @example
188
+ # require "dry/inflector"
189
+ #
190
+ # inflector = Dry::Inflector.new
191
+ # inflector.ordinalize(1) # => "1st"
192
+ # inflector.ordinalize(2) # => "2nd"
193
+ # inflector.ordinalize(3) # => "3rd"
194
+ # inflector.ordinalize(10) # => "10th"
195
+ # inflector.ordinalize(23) # => "23rd"
196
+ def ordinalize(number) # rubocop:disable Metrics/MethodLength
197
+ abs_value = number.abs
198
+
199
+ if ORDINALIZE_TH.key?(abs_value % 100)
200
+ "#{number}th"
201
+ else
202
+ case abs_value % 10
203
+ when 1 then "#{number}st"
204
+ when 2 then "#{number}nd"
205
+ when 3 then "#{number}rd"
206
+ else "#{number}th"
207
+ end
208
+ end
209
+ end
210
+
211
+ # Pluralize a string
212
+ #
213
+ # @param input [String,Symbol] the input
214
+ # @return [String] the pluralized string
215
+ #
216
+ # @since 0.1.0
217
+ #
218
+ # @example
219
+ # require "dry/inflector"
220
+ #
221
+ # inflector = Dry::Inflector.new
222
+ # inflector.pluralize("book") # => "books"
223
+ # inflector.pluralize("money") # => "money"
224
+ def pluralize(input)
225
+ input = input.to_s
226
+ return input if uncountable?(input)
227
+ inflections.plurals.apply_to(input)
228
+ end
229
+
230
+ # Singularize a string
231
+ #
232
+ # @param input [String] the input
233
+ # @return [String] the singularized string
234
+ #
235
+ # @since 0.1.0
236
+ #
237
+ # @example
238
+ # require "dry/inflector"
239
+ #
240
+ # inflector = Dry::Inflector.new
241
+ # inflector.singularize("books") # => "book"
242
+ # inflector.singularize("money") # => "money"
243
+ def singularize(input)
244
+ input = input.to_s
245
+ return input if uncountable?(input)
246
+ inflections.singulars.apply_to(input)
247
+ end
248
+
249
+ # Tableize a string
250
+ #
251
+ # @param input [String,Symbol] the input
252
+ # @return [String] the tableized string
253
+ #
254
+ # @since 0.1.0
255
+ #
256
+ # @example
257
+ # require "dry/inflector"
258
+ #
259
+ # inflector = Dry::Inflector.new
260
+ # inflector.tableize("Book") # => "books"
261
+ def tableize(input)
262
+ input = input.to_s.gsub(/::/, "_")
263
+ pluralize(underscore(input))
264
+ end
265
+
266
+ # Underscore a string
267
+ #
268
+ # @param input [String,Symbol] the input
269
+ # @return [String] the underscored string
270
+ #
271
+ # @since 0.1.0
272
+ #
273
+ # @example
274
+ # require "dry/inflector"
275
+ #
276
+ # inflector = Dry::Inflector.new
277
+ # inflector.underscore("dry-inflector") # => "dry_inflector"
278
+ def underscore(input)
279
+ input = input.to_s.gsub("::", "/")
280
+ input.gsub!(inflections.acronyms.regex) do
281
+ m1 = Regexp.last_match(1)
282
+ m2 = Regexp.last_match(2)
283
+ "#{m1 ? '_' : '' }#{m2.downcase}"
284
+ end
285
+ input.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
286
+ input.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
287
+ input.tr!("-", "_")
288
+ input.downcase!
289
+ input
290
+ end
291
+
292
+ # Check if the input is an uncountable word
293
+ #
294
+ # @param input [String] the input
295
+ # @return [TrueClass,FalseClass] the result of the check
296
+ #
297
+ # @since 0.1.0
298
+ # @api private
299
+ def uncountable?(input)
300
+ !(input =~ /\A[[:space:]]*\z/).nil? || inflections.uncountables.include?(input.downcase)
301
+ end
302
+
303
+ # @return [String]
304
+ #
305
+ # @since 0.2.0
306
+ # @api public
307
+ def to_s
308
+ '#<Dry::Inflector>'
309
+ end
310
+ alias inspect to_s
311
+
312
+ private
313
+
314
+ # @since 0.1.0
315
+ # @api private
316
+ ORDINALIZE_TH = (11..13).each_with_object({}) { |n, ret| ret[n] = true }.freeze
317
+
318
+ # @since 0.1.2
319
+ # @api private
320
+ DEFAULT_SEPARATOR = " "
321
+
322
+ attr_reader :inflections
323
+
324
+ # @since 0.1.3
325
+ # @api private
326
+ def internal_camelize(input, upper)
327
+ input = input.to_s.dup
328
+ input.sub!(/^[a-z\d]*/) { |match| inflections.acronyms.apply_to(match, upper) }
329
+ input.gsub!(%r{(?:_|(/))([a-z\d]*)}i) do
330
+ m1 = Regexp.last_match(1)
331
+ m2 = Regexp.last_match(2)
332
+ "#{m1}#{inflections.acronyms.apply_to(m2)}"
333
+ end
334
+ input.gsub!("/", "::")
335
+ input
336
+ end
337
+
338
+ end
339
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class Inflector
5
+ # A set of acronyms
6
+ #
7
+ # @since 0.1.2
8
+ # @api private
9
+ class Acronyms
10
+ attr_reader :regex
11
+
12
+ # @since 0.1.2
13
+ # @api private
14
+ def initialize
15
+ @rules = {}
16
+ define_regex_patterns
17
+ end
18
+
19
+ # @since 0.1.2
20
+ # @api private
21
+ def apply_to(word, capitalize = true)
22
+ @rules[word.downcase] || (capitalize ? word.capitalize : word)
23
+ end
24
+
25
+ # @since 0.1.2
26
+ # @api private
27
+ def add(rule, replacement)
28
+ @rules[rule] = replacement
29
+ define_regex_patterns
30
+ end
31
+
32
+ private
33
+
34
+ # @since 0.1.2
35
+ # @api private
36
+ def define_regex_patterns
37
+ regex = @rules.empty? ? /(?=a)b/ : /#{@rules.values.join("|")}/
38
+ @regex = /(?:(?<=([A-Za-z\d]))|\b)(#{regex})(?=\b|[^a-z])/
39
+ end
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,249 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+ require "dry/inflector/rules"
5
+ require "dry/inflector/acronyms"
6
+
7
+ module Dry
8
+ class Inflector
9
+ # Inflections
10
+ #
11
+ # @since 0.1.0
12
+ class Inflections
13
+ require "dry/inflector/inflections/defaults"
14
+
15
+ # Instantiate a set of inflection rules.
16
+ # It adds the default rules and the optional customizations, passed as a block.
17
+ #
18
+ # @param blk [Proc] the optional, custom rules
19
+ #
20
+ # @since 0.1.0
21
+ # @api private
22
+ def self.build(&blk)
23
+ new do |inflect|
24
+ Defaults.call(inflect)
25
+ blk.call(inflect) if block_given?
26
+ end
27
+ end
28
+
29
+ # Pluralization rules
30
+ #
31
+ # @return [Dry::Inflector::Rules]
32
+ #
33
+ # @since 0.1.0
34
+ # @api private
35
+ attr_reader :plurals
36
+
37
+ # Singularization rules
38
+ #
39
+ # @return [Dry::Inflector::Rules]
40
+ #
41
+ # @since 0.1.0
42
+ # @api private
43
+ attr_reader :singulars
44
+
45
+ # Uncountable rules
46
+ #
47
+ # @return [Set]
48
+ #
49
+ # @since 0.1.0
50
+ # @api private
51
+ attr_reader :uncountables
52
+
53
+ # Human rules
54
+ #
55
+ # @return [Dry::Inflector::Rules]
56
+ #
57
+ # @since 0.1.0
58
+ # @api private
59
+ attr_reader :humans
60
+
61
+ # Acronyms
62
+ #
63
+ # @return [Dry::Inflector::Acronyms]
64
+ #
65
+ # @since 0.1.2
66
+ # @api private
67
+ attr_reader :acronyms
68
+
69
+ # Instantiate the rules
70
+ #
71
+ # @return [Dry::Inflector::Inflections]
72
+ # @yieldparam [self]
73
+ #
74
+ # @since 0.1.0
75
+ # @api private
76
+ def initialize
77
+ @plurals = Rules.new
78
+ @singulars = Rules.new
79
+ @humans = Rules.new
80
+ @uncountables = Set[]
81
+ @acronyms = Acronyms.new
82
+
83
+ yield(self) if block_given?
84
+ end
85
+
86
+ # Add a custom pluralization rule
87
+ #
88
+ # Specifies a new pluralization rule and its replacement.
89
+ # The rule can either be a string or a regular expression.
90
+ # The replacement should always be a string that may include references to the matched data from the rule.
91
+ #
92
+ # @param rule [String, Regexp] the rule
93
+ # @param replacement [String] the replacement
94
+ #
95
+ # @since 0.1.0
96
+ #
97
+ # @example
98
+ # require "dry/inflector"
99
+ #
100
+ # inflector = Dry::Inflector.new do |inflections|
101
+ # inflections.plural "virus", "viruses"
102
+ # end
103
+ def plural(rule, replacement)
104
+ rule(rule, replacement, plurals)
105
+ end
106
+
107
+ # Add a custom singularization rule
108
+ #
109
+ # Specifies a new singularization rule and its replacement.
110
+ # The rule can either be a string or a regular expression.
111
+ # The replacement should always be a string that may include references to the matched data from the rule.
112
+ #
113
+ # @param rule [String, Regexp] the rule
114
+ # @param replacement [String] the replacement
115
+ #
116
+ # @since 0.1.0
117
+ #
118
+ # @example
119
+ # require "dry/inflector"
120
+ #
121
+ # inflector = Dry::Inflector.new do |inflections|
122
+ # inflections.singular "thieves", "thief"
123
+ # end
124
+ def singular(rule, replacement)
125
+ rule(rule, replacement, singulars)
126
+ end
127
+
128
+ # Add a custom pluralization rule
129
+ #
130
+ # Specifies a new irregular that applies to both pluralization and singularization at the same time.
131
+ # This can only be used for strings, not regular expressions.
132
+ # You simply pass the irregular in singular and plural form.
133
+ #
134
+ # @param singular [String] the singular
135
+ # @param plural [String] the plural
136
+ #
137
+ # @since 0.1.0
138
+ #
139
+ # @example
140
+ # require "dry/inflector"
141
+ #
142
+ # inflector = Dry::Inflector.new do |inflections|
143
+ # inflections.singular "octopus", "octopi"
144
+ # end
145
+ def irregular(singular, plural)
146
+ uncountables.delete(singular)
147
+ uncountables.delete(plural)
148
+
149
+ add_irregular(singular, plural, plurals)
150
+ add_irregular(plural, singular, singulars)
151
+ end
152
+
153
+ # Add a custom rule for uncountable words
154
+ #
155
+ # Uncountable will not be inflected
156
+ #
157
+ # @param [Enumerable<String>] words
158
+ #
159
+ # @since 0.1.0
160
+ #
161
+ # @example
162
+ # require "dry/inflector"
163
+ #
164
+ # inflector = Dry::Inflector.new do |inflections|
165
+ # inflections.uncountable "money"
166
+ # inflections.uncountable "money", "information"
167
+ # inflections.uncountable %w(money information rice)
168
+ # end
169
+ def uncountable(*words)
170
+ uncountables.merge(words.flatten)
171
+ end
172
+
173
+ # Add one or more acronyms
174
+ #
175
+ # Acronyms affect how basic operations are performed, such
176
+ # as camelize/underscore.
177
+ #
178
+ # @param words [Array<String>] a list of acronyms
179
+ #
180
+ # @since 0.1.2
181
+ #
182
+ # @example
183
+ # require "dry/inflector"
184
+ #
185
+ # inflector = Dry::Inflector.new do |inflections|
186
+ # inflections.acronym "HTML"
187
+ # end
188
+ #
189
+ # inflector.camelize("html") # => "HTML"
190
+ # inflector.underscore("HTMLIsFun") # => "html_is_fun"
191
+ def acronym(*words)
192
+ words.each { |word| @acronyms.add(word.downcase, word) }
193
+ end
194
+
195
+ # Add a custom humanize rule
196
+ #
197
+ # Specifies a humanized form of a string by a regular expression rule or by a string mapping.
198
+ # When using a regular expression based replacement, the normal humanize formatting is called after the replacement.
199
+ # When a string is used, the human form should be specified as desired (example: `"The name"`, not `"the_name"`)
200
+ #
201
+ # @param rule [String, Regexp] the rule
202
+ # @param replacement [String] the replacement
203
+ #
204
+ # @since 0.1.0
205
+ #
206
+ # @example
207
+ # require "dry/inflector"
208
+ #
209
+ # inflector = Dry::Inflector.new do |inflections|
210
+ # inflections.human(/_cnt$/i, '\1_count')
211
+ # inflections.human("legacy_col_person_name", "Name")
212
+ # end
213
+ def human(rule, replacement)
214
+ humans.insert(0, [rule, replacement])
215
+ end
216
+
217
+ private
218
+
219
+ # Add irregular inflection
220
+ #
221
+ # @param rule [String] the rule
222
+ # @param replacement [String] the replacement
223
+ #
224
+ # @return [undefined]
225
+ #
226
+ # @since 0.1.0
227
+ # @api private
228
+ def add_irregular(rule, replacement, target)
229
+ head, *tail = rule.chars.to_a
230
+ rule(/(#{head})#{tail.join}\z/i, '\1' + replacement[1..-1], target)
231
+ end
232
+
233
+ # Add a new rule
234
+ #
235
+ # @param rule [String, Regexp] the rule
236
+ # @param replacement [String, Regexp] the replacement
237
+ # @param target [Dry::Inflector::Rules] the target
238
+ #
239
+ # @since 0.1.0
240
+ # @api private
241
+ def rule(rule, replacement, target)
242
+ uncountables.delete(rule)
243
+ uncountables.delete(replacement)
244
+
245
+ target.insert(0, [rule, replacement])
246
+ end
247
+ end
248
+ end
249
+ end
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class Inflector
5
+ class Inflections
6
+ # Default inflections
7
+ #
8
+ # @since 0.1.0
9
+ # @api private
10
+ #
11
+ # rubocop:disable Metrics/AbcSize
12
+ # rubocop:disable Metrics/MethodLength
13
+ module Defaults
14
+ # @since 0.1.0
15
+ # @api private
16
+ def self.call(inflect)
17
+ plural(inflect)
18
+ singular(inflect)
19
+ irregular(inflect)
20
+ uncountable(inflect)
21
+ acronyms(inflect)
22
+ end
23
+
24
+ # @since 0.1.0
25
+ # @api private
26
+ def self.plural(inflect)
27
+ inflect.plural(/\z/, "s")
28
+ inflect.plural(/s\z/i, "s")
29
+ inflect.plural(/(ax|test)is\z/i, '\1es')
30
+ inflect.plural(/(.*)us\z/i, '\1uses')
31
+ inflect.plural(/(octop|vir|cact)us\z/i, '\1i')
32
+ inflect.plural(/(octop|vir)i\z/i, '\1i')
33
+ inflect.plural(/(alias|status)\z/i, '\1es')
34
+ inflect.plural(/(buffal|domin|ech|embarg|her|mosquit|potat|tomat)o\z/i, '\1oes')
35
+ inflect.plural(/(?<!b)um\z/i, '\1a')
36
+ inflect.plural(/([ti])a\z/i, '\1a')
37
+ inflect.plural(/sis\z/i, "ses")
38
+ inflect.plural(/(.*)(?:([^f]))f[e]*\z/i, '\1\2ves')
39
+ inflect.plural(/(hive|proof)\z/i, '\1s') # TODO: proof can be moved in the above regexp
40
+ inflect.plural(/([^aeiouy]|qu)y\z/i, '\1ies')
41
+ inflect.plural(/(x|ch|ss|sh)\z/i, '\1es')
42
+ inflect.plural(/(stoma|epo)ch\z/i, '\1chs')
43
+ inflect.plural(/(matr|vert|ind)(?:ix|ex)\z/i, '\1ices')
44
+ inflect.plural(/([m|l])ouse\z/i, '\1ice')
45
+ inflect.plural(/([m|l])ice\z/i, '\1ice')
46
+ inflect.plural(/^(ox)\z/i, '\1en')
47
+ inflect.plural(/^(oxen)\z/i, '\1')
48
+ inflect.plural(/(quiz)\z/i, '\1zes')
49
+ inflect.plural(/(.*)non\z/i, '\1na')
50
+ inflect.plural(/(.*)ma\z/i, '\1mata')
51
+ inflect.plural(/(.*)(eau|eaux)\z/, '\1eaux')
52
+ end
53
+
54
+ # @since 0.1.0
55
+ # @api private
56
+ def self.singular(inflect)
57
+ inflect.singular(/s\z/i, "")
58
+ inflect.singular(/(n)ews\z/i, '\1ews')
59
+ inflect.singular(/([ti])a\z/i, '\1um')
60
+ inflect.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)\z/i, '\1\2sis')
61
+ inflect.singular(/(^analy)(sis|ses)\z/i, '\1sis')
62
+ inflect.singular(/([^f])ves\z/i, '\1fe')
63
+ inflect.singular(/(hive)s\z/i, '\1')
64
+ inflect.singular(/(tive)s\z/i, '\1')
65
+ inflect.singular(/([lr])ves\z/i, '\1f')
66
+ inflect.singular(/([^aeiouy]|qu)ies\z/i, '\1y')
67
+ inflect.singular(/(s)eries\z/i, '\1eries')
68
+ inflect.singular(/(m)ovies\z/i, '\1ovie')
69
+ inflect.singular(/(ss)\z/i, '\1')
70
+ inflect.singular(/(x|ch|ss|sh)es\z/i, '\1')
71
+ inflect.singular(/([m|l])ice\z/i, '\1ouse')
72
+ inflect.singular(/(bus)(es)?\z/i, '\1')
73
+ inflect.singular(/(o)es\z/i, '\1')
74
+ inflect.singular(/(shoe)s\z/i, '\1')
75
+ inflect.singular(/(cris|ax|test)(is|es)\z/i, '\1is')
76
+ inflect.singular(/(octop|vir)(us|i)\z/i, '\1us')
77
+ inflect.singular(/(alias|status)(es)?\z/i, '\1')
78
+ inflect.singular(/^(ox)en/i, '\1')
79
+ inflect.singular(/(vert|ind)ices\z/i, '\1ex')
80
+ inflect.singular(/(matr)ices\z/i, '\1ix')
81
+ inflect.singular(/(quiz)zes\z/i, '\1')
82
+ inflect.singular(/(database)s\z/i, '\1')
83
+ end
84
+
85
+ # @since 0.1.0
86
+ # @api private
87
+ def self.irregular(inflect)
88
+ inflect.irregular("person", "people")
89
+ inflect.irregular("man", "men")
90
+ inflect.irregular("human", "humans") # NOTE: this is here only to override the previous rule
91
+ inflect.irregular("child", "children")
92
+ inflect.irregular("sex", "sexes")
93
+ inflect.irregular("foot", "feet")
94
+ inflect.irregular("tooth", "teeth")
95
+ inflect.irregular("goose", "geese")
96
+ inflect.irregular("forum", "forums") # FIXME: this is here because I need to fix the "um" regexp
97
+ end
98
+
99
+ # @since 0.1.0
100
+ # @api private
101
+ def self.uncountable(inflect)
102
+ inflect.uncountable(%w[hovercraft moose deer milk rain Swiss grass equipment information rice money species series fish sheep jeans])
103
+ end
104
+
105
+ # @since 0.1.2
106
+ # @api private
107
+ def self.acronyms(inflect)
108
+ inflect.acronym(*%w[JSON HTTP OpenSSL HMAC])
109
+ end
110
+
111
+ private_class_method :plural, :singular, :irregular, :uncountable, :acronyms
112
+ end
113
+ # rubocop:enable Metrics/MethodLength
114
+ # rubocop:enable Metrics/AbcSize
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class Inflector
5
+ # A set of inflection rules
6
+ #
7
+ # @since 0.1.0
8
+ # @api private
9
+ class Rules
10
+ # @since 0.1.0
11
+ # @api private
12
+ def initialize
13
+ @rules = []
14
+ end
15
+
16
+ # @since 0.1.0
17
+ # @api private
18
+ def apply_to(word)
19
+ result = word.dup
20
+ each { |rule, replacement| break if result.gsub!(rule, replacement) }
21
+ result
22
+ end
23
+
24
+ # @since 0.1.0
25
+ # @api private
26
+ def insert(index, array)
27
+ @rules.insert(index, array)
28
+ end
29
+
30
+ # @since 0.1.0
31
+ # @api private
32
+ def each(&blk)
33
+ @rules.each(&blk)
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dry
4
+ class Inflector
5
+ # @since 0.1.0
6
+ VERSION = "0.2.0"
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dry-inflector
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.0
5
+ platform: ruby
6
+ authors:
7
+ - Luca Guidi
8
+ - Andrii Savchenko
9
+ - Abinoam P. Marques Jr.
10
+ autorequire:
11
+ bindir: exe
12
+ cert_chain: []
13
+ date: 2019-10-13 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - ">="
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - ">="
27
+ - !ruby/object:Gem::Version
28
+ version: '0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rake
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - "~>"
34
+ - !ruby/object:Gem::Version
35
+ version: '12.0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - "~>"
41
+ - !ruby/object:Gem::Version
42
+ version: '12.0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: rspec
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - "~>"
48
+ - !ruby/object:Gem::Version
49
+ version: '3.7'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - "~>"
55
+ - !ruby/object:Gem::Version
56
+ version: '3.7'
57
+ - !ruby/object:Gem::Dependency
58
+ name: rubocop
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - "~>"
62
+ - !ruby/object:Gem::Version
63
+ version: 0.50.0
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - "~>"
69
+ - !ruby/object:Gem::Version
70
+ version: 0.50.0
71
+ description: String inflections for dry-rb
72
+ email:
73
+ - me@lucaguidi.com
74
+ - andrey@aejis.eu
75
+ - abinoam@gmail.com
76
+ executables: []
77
+ extensions: []
78
+ extra_rdoc_files: []
79
+ files:
80
+ - CHANGELOG.md
81
+ - LICENSE.md
82
+ - README.md
83
+ - dry-inflector.gemspec
84
+ - lib/dry-inflector.rb
85
+ - lib/dry/inflector.rb
86
+ - lib/dry/inflector/acronyms.rb
87
+ - lib/dry/inflector/inflections.rb
88
+ - lib/dry/inflector/inflections/defaults.rb
89
+ - lib/dry/inflector/rules.rb
90
+ - lib/dry/inflector/version.rb
91
+ homepage: https://dry-rb.org
92
+ licenses:
93
+ - MIT
94
+ metadata:
95
+ allowed_push_host: https://rubygems.org
96
+ changelog_uri: https://github.com/dry-rb/dry-inflector/blob/master/CHANGELOG.md
97
+ source_code_uri: https://github.com/dry-rb/dry-inflector
98
+ bug_tracker_uri: https://github.com/dry-rb/dry-inflector/issues
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '2.4'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.0.3
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: DRY Inflector
118
+ test_files: []