dry-inflector 0.2.0

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