Module: Class2::Inflector

Extended by:
Inflector
Included in:
Inflector
Defined in:
lib/class2/inflector.rb

Overview

String inflection used to convert attribute names into class names, method names, and JSON keys.

dry-inflector provides the rule engine, but it is loaded with ActiveSupport 6.1's rules and case conversion so that the names Class2 generates are the same as when ActiveSupport provided the inflector. dry-inflector's rules, acronyms (API, JSON, HTTP, ...), and uncountables differ from ActiveSupport's and would change class names and plural attribute detection.

Constant Summary collapse

UNCOUNTABLE =

ActiveSupport's uncountables.

%w[
  equipment information rice money species series fish sheep jeans police
].freeze
INFLECTOR =

Rules are prepended, so the last one added is the first one applied and they are added here in ActiveSupport's order so they have its precedence.

Dry::Inflector.new do |inflections|
  inflections.plural(/$/, "s")
  inflections.plural(/s$/i, "s")
  inflections.plural(/^(ax|test)is$/i, '\1es')
  inflections.plural(/(octop|vir)us$/i, '\1i')
  inflections.plural(/(octop|vir)i$/i, '\1i')
  inflections.plural(/(alias|status)$/i, '\1es')
  inflections.plural(/(bu)s$/i, '\1ses')
  inflections.plural(/(buffal|tomat)o$/i, '\1oes')
  inflections.plural(/([ti])um$/i, '\1a')
  inflections.plural(/([ti])a$/i, '\1a')
  inflections.plural(/sis$/i, "ses")
  inflections.plural(/(?:([^f])fe|([lr])f)$/i, '\1\2ves')
  inflections.plural(/(hive)$/i, '\1s')
  inflections.plural(/([^aeiouy]|qu)y$/i, '\1ies')
  inflections.plural(/(x|ch|ss|sh)$/i, '\1es')
  inflections.plural(/(matr|vert|ind)(?:ix|ex)$/i, '\1ices')
  inflections.plural(/^(m|l)ouse$/i, '\1ice')
  inflections.plural(/^(m|l)ice$/i, '\1ice')
  inflections.plural(/^(ox)$/i, '\1en')
  inflections.plural(/^(oxen)$/i, '\1')
  inflections.plural(/(quiz)$/i, '\1zes')

  # ActiveSupport leaves a word alone when none of its rules match. These
  # rules have a higher precedence than dry-inflector's defaults, which have
  # irregulars ActiveSupport doesn't (foot/feet, tooth/teeth, goose/geese),
  # so this rule -- the one with the lowest precedence -- matches everything
  # and leaves it alone.
  inflections.singular(/$/, '\0')
  inflections.singular(/s$/i, "")
  inflections.singular(/(ss)$/i, '\1')
  inflections.singular(/(n)ews$/i, '\1ews')
  inflections.singular(/([ti])a$/i, '\1um')
  inflections.singular(/((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)(sis|ses)$/i, '\1sis')
  inflections.singular(/(^analy)(sis|ses)$/i, '\1sis')
  inflections.singular(/([^f])ves$/i, '\1fe')
  inflections.singular(/(hive)s$/i, '\1')
  inflections.singular(/(tive)s$/i, '\1')
  inflections.singular(/([lr])ves$/i, '\1f')
  inflections.singular(/([^aeiouy]|qu)ies$/i, '\1y')
  inflections.singular(/(s)eries$/i, '\1eries')
  inflections.singular(/(m)ovies$/i, '\1ovie')
  inflections.singular(/(x|ch|ss|sh)es$/i, '\1')
  inflections.singular(/^(m|l)ice$/i, '\1ouse')
  inflections.singular(/(bus)(es)?$/i, '\1')
  inflections.singular(/(o)es$/i, '\1')
  inflections.singular(/(shoe)s$/i, '\1')
  inflections.singular(/(cris|test)(is|es)$/i, '\1is')
  inflections.singular(/^(a)x[ie]s$/i, '\1xis')
  inflections.singular(/(octop|vir)(us|i)$/i, '\1us')
  inflections.singular(/(alias|status)(es)?$/i, '\1')
  inflections.singular(/^(ox)en/i, '\1')
  inflections.singular(/(vert|ind)ices$/i, '\1ex')
  inflections.singular(/(matr)ices$/i, '\1ix')
  inflections.singular(/(quiz)zes$/i, '\1')
  inflections.singular(/(database)s$/i, '\1')

  # ActiveSupport adds a rule for both forms of an irregular, so that the
  # plural pluralizes to itself ("people" => "people"). Every word here has
  # the same first letter in both forms, which is all ActiveSupport's rules
  # for this case need.
  {
    "person" => "people",
    "man" => "men",
    "child" => "children",
    "sex" => "sexes",
    "move" => "moves",
    "zombie" => "zombies"
  }.each do |singular, plural|
    s0, srest = singular[0], singular[1..-1]
    p0, prest = plural[0], plural[1..-1]

    inflections.plural(/(#{s0})#{srest}$/i, "\\1#{prest}")
    inflections.plural(/(#{p0})#{prest}$/i, "\\1#{prest}")

    inflections.singular(/(#{s0})#{srest}$/i, "\\1#{srest}")
    inflections.singular(/(#{p0})#{prest}$/i, "\\1#{srest}")
  end

  # dry-inflector considers a word uncountable if any of its
  # underscore-separated segments is ("user_money"), and its list has words
  # ActiveSupport's doesn't (moose, deer, ...). Drop it and use #uncountable?
  # below instead.
  inflections.uncountables.clear
end

Instance Method Summary collapse

Instance Method Details

#camelize(word, lower = false) ⇒ Object

"some_value" => "SomeValue", "some_value", true => "someValue". "/" becomes "::".

Unlike ActiveSupport::Inflector#camelize this has no acronym support, so there's no need to pass a symbol to get "lower" camel case.



139
140
141
142
143
144
145
146
147
148
149
150
151
# File 'lib/class2/inflector.rb', line 139

def camelize(word, lower = false)
  word = word.to_s.dup

  if lower
    word.sub!(/^\w/) { |c| c.downcase }
  else
    word.sub!(/^[a-z\d]*/) { |c| c.capitalize }
  end

  word.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{$2.capitalize}" }
  word.gsub!("/", "::")
  word
end

#classify(word) ⇒ Object

Convert a table, attribute, or method name into a class name. "user_statuses" => "UserStatus", "admin/user" => "Admin::User".



128
129
130
# File 'lib/class2/inflector.rb', line 128

def classify(word)
  camelize(singularize(word.to_s.sub(/.*\./, "")))
end

#pluralize(word) ⇒ Object



114
115
116
117
# File 'lib/class2/inflector.rb', line 114

def pluralize(word)
  word = word.to_s
  uncountable?(word) ? word : INFLECTOR.pluralize(word)
end

#singularize(word) ⇒ Object



119
120
121
122
# File 'lib/class2/inflector.rb', line 119

def singularize(word)
  word = word.to_s
  uncountable?(word) ? word : INFLECTOR.singularize(word)
end

#underscore(word) ⇒ Object

"SomeValue" => "some_value", "APIClient" => "api_client", "Admin::User" => "admin/user".



157
158
159
160
161
162
163
164
165
166
167
# File 'lib/class2/inflector.rb', line 157

def underscore(word)
  word = word.to_s.dup
  return word unless word =~ /[A-Z-]|::/

  word.gsub!("::", "/")
  word.gsub!(/([A-Z\d]+)([A-Z][a-z])/, '\1_\2')
  word.gsub!(/([a-z\d])([A-Z])/, '\1_\2')
  word.tr!("-", "_")
  word.downcase!
  word
end