naming.rb 9.5 KB
Newer Older
1
require 'active_support/core_ext/hash/except'
2
require 'active_support/core_ext/module/introspection'
3

J
Joshua Peek 已提交
4
module ActiveModel
5 6 7
  class Name
    include Comparable

8
    attr_reader :singular, :plural, :element, :collection,
9 10
      :singular_route_key, :route_key, :param_key, :i18n_key,
      :name
J
José Valim 已提交
11

12
    alias_method :cache_key, :collection
13

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
    ##
    # :method: ==
    #
    # :call-seq:
    #   ==(other)
    #
    # Equivalent to <tt>String#==</tt>. Returns +true+ if the class name and
    # +other+ are equal, otherwise +false+.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
    #
    #   BlogPost.model_name == 'BlogPost'  # => true
    #   BlogPost.model_name == 'Blog Post' # => false

    ##
    # :method: ===
    #
    # :call-seq:
    #   ===(other)
    #
    # Equivalent to <tt>#==</tt>.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
    #
    #   BlogPost.model_name === 'BlogPost'  # => true
    #   BlogPost.model_name === 'Blog Post' # => false

    ##
    # :method: <=>
    #
    # :call-seq:
    #   ==(other)
    #
    # Equivalent to <tt>String#<=></tt>.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
    #
    #   BlogPost.model_name <=> 'BlogPost'  # => 0
Y
Yves Senn 已提交
58 59
    #   BlogPost.model_name <=> 'Blog'      # => 1
    #   BlogPost.model_name <=> 'BlogPosts' # => -1
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130

    ##
    # :method: =~
    #
    # :call-seq:
    #   =~(regexp)
    #
    # Equivalent to <tt>String#=~</tt>. Match the class name against the given
    # regexp. Returns the position where the match starts or +nil+ if there is
    # no match.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
    #
    #   BlogPost.model_name =~ /Post/ # => 4
    #   BlogPost.model_name =~ /\d/   # => nil

    ##
    # :method: !~
    #
    # :call-seq:
    #   !~(regexp)
    #
    # Equivalent to <tt>String#!~</tt>. Match the class name against the given
    # regexp. Returns +true+ if there is no match, otherwise +false+.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
    #
    #   BlogPost.model_name !~ /Post/ # => false
    #   BlogPost.model_name !~ /\d/   # => true

    ##
    # :method: eql?
    #
    # :call-seq:
    #   eql?(other)
    #
    # Equivalent to <tt>String#eql?</tt>. Returns +true+ if the class name and
    # +other+ have the same length and content, otherwise +false+.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
    #
    #   BlogPost.model_name.eql?('BlogPost')  # => true
    #   BlogPost.model_name.eql?('Blog Post') # => false

    ##
    # :method: to_s
    #
    # :call-seq:
    #   to_s()
    #
    # Returns the class name.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
    #
    #   BlogPost.model_name.to_s # => "BlogPost"

    ##
    # :method: to_str
    #
    # :call-seq:
    #   to_str()
    #
    # Equivalent to +to_s+.
131
    delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
132
             :to_str, to: :name
133

134 135 136 137 138 139 140 141 142 143 144
    # Returns a new ActiveModel::Name instance. By default, the +namespace+
    # and +name+ option will take the namespace and name of the given class
    # respectively.
    #
    #   module Foo
    #     class Bar
    #     end
    #   end
    #
    #   ActiveModel::Name.new(Foo::Bar).to_s
    #   # => "Foo::Bar"
145 146
    def initialize(klass, namespace = nil, name = nil)
      @name = name || klass.name
147

148
      raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
149

150
      @unnamespaced = @name.sub(/^#{namespace.name}::/, '') if namespace
151
      @klass        = klass
152 153 154 155 156 157
      @singular     = _singularize(@name)
      @plural       = ActiveSupport::Inflector.pluralize(@singular)
      @element      = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
      @human        = ActiveSupport::Inflector.humanize(@element)
      @collection   = ActiveSupport::Inflector.tableize(@name)
      @param_key    = (namespace ? _singularize(@unnamespaced) : @singular)
158
      @i18n_key     = @name.underscore.to_sym
J
José Valim 已提交
159 160

      @route_key          = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
161
      @singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
J
José Valim 已提交
162
      @route_key << "_index" if @plural == @singular
163
    end
164 165

    # Transform the model name into a more humane format, using I18n. By default,
166 167 168 169 170
    # it will underscore then humanize the class name.
    #
    #   class BlogPost
    #     extend ActiveModel::Naming
    #   end
171 172 173
    #
    #   BlogPost.model_name.human # => "Blog post"
    #
174 175 176 177 178 179
    # Specify +options+ with additional translating options.
    def human(options={})
      return @human unless @klass.respond_to?(:lookup_ancestors) &&
                           @klass.respond_to?(:i18n_scope)

      defaults = @klass.lookup_ancestors.map do |klass|
180 181
        klass.model_name.i18n_key
      end
182

183
      defaults << options[:default] if options[:default]
184 185
      defaults << @human

186
      options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
187 188
      I18n.translate(defaults.shift, options)
    end
189 190

    private
191 192 193 194

    def _singularize(string, replacement='_')
      ActiveSupport::Inflector.underscore(string).tr('/', replacement)
    end
195
  end
R
Rizwan Reza 已提交
196

197
  # == Active \Model \Naming
R
Rizwan Reza 已提交
198 199
  #
  # Creates a +model_name+ method on your object.
200
  #
201
  # To implement, just extend ActiveModel::Naming in your object:
202
  #
203
  #   class BookCover
204
  #     extend ActiveModel::Naming
205
  #   end
206
  #
207 208
  #   BookCover.model_name        # => "BookCover"
  #   BookCover.model_name.human  # => "Book cover"
209
  #
210 211
  #   BookCover.model_name.i18n_key              # => :book_cover
  #   BookModule::BookCover.model_name.i18n_key  # => :"book_module/book_cover"
212
  #
213
  # Providing the functionality that ActiveModel::Naming provides in your object
214 215
  # is required to pass the Active Model Lint test. So either extending the
  # provided method below, or rolling your own is required.
J
Joshua Peek 已提交
216 217
  module Naming
    # Returns an ActiveModel::Name object for module. It can be
218 219
    # used to retrieve all kinds of naming-related information
    # (See ActiveModel::Name for more information).
220 221 222 223 224 225 226 227
    #
    #   class Person < ActiveModel::Model
    #   end
    #
    #   Person.model_name          # => Person
    #   Person.model_name.class    # => ActiveModel::Name
    #   Person.model_name.singular # => "person"
    #   Person.model_name.plural   # => "people"
J
Joshua Peek 已提交
228
    def model_name
T
thedarkone 已提交
229
      @_model_name ||= begin
230 231 232
        namespace = self.parents.detect do |n|
          n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
        end
T
thedarkone 已提交
233 234
        ActiveModel::Name.new(self, namespace)
      end
J
Joshua Peek 已提交
235
    end
236

237
    # Returns the plural class name of a record or class.
238 239 240 241 242 243 244
    #
    #   ActiveModel::Naming.plural(post)             # => "posts"
    #   ActiveModel::Naming.plural(Highrise::Person) # => "highrise_people"
    def self.plural(record_or_class)
      model_name_from_record_or_class(record_or_class).plural
    end

245
    # Returns the singular class name of a record or class.
246 247 248 249 250 251 252
    #
    #   ActiveModel::Naming.singular(post)             # => "post"
    #   ActiveModel::Naming.singular(Highrise::Person) # => "highrise_person"
    def self.singular(record_or_class)
      model_name_from_record_or_class(record_or_class).singular
    end

253
    # Identifies whether the class name of a record or class is uncountable.
254 255
    #
    #   ActiveModel::Naming.uncountable?(Sheep) # => true
256
    #   ActiveModel::Naming.uncountable?(Post)  # => false
257 258 259 260
    def self.uncountable?(record_or_class)
      plural(record_or_class) == singular(record_or_class)
    end

J
José Valim 已提交
261 262 263
    # Returns string to use while generating route names. It differs for
    # namespaced models regarding whether it's inside isolated engine.
    #
264
    #   # For isolated engine:
265
    #   ActiveModel::Naming.singular_route_key(Blog::Post) #=> "post"
J
José Valim 已提交
266
    #
267
    #   # For shared engine:
268
    #   ActiveModel::Naming.singular_route_key(Blog::Post) #=> "blog_post"
J
José Valim 已提交
269 270 271 272
    def self.singular_route_key(record_or_class)
      model_name_from_record_or_class(record_or_class).singular_route_key
    end

273 274 275
    # Returns string to use while generating route names. It differs for
    # namespaced models regarding whether it's inside isolated engine.
    #
276
    #   # For isolated engine:
277
    #   ActiveModel::Naming.route_key(Blog::Post) #=> "posts"
278
    #
279
    #   # For shared engine:
280
    #   ActiveModel::Naming.route_key(Blog::Post) #=> "blog_posts"
J
José Valim 已提交
281 282 283
    #
    # The route key also considers if the noun is uncountable and, in
    # such cases, automatically appends _index.
284 285 286 287
    def self.route_key(record_or_class)
      model_name_from_record_or_class(record_or_class).route_key
    end

288 289 290
    # Returns string to use for params names. It differs for
    # namespaced models regarding whether it's inside isolated engine.
    #
291
    #   # For isolated engine:
292
    #   ActiveModel::Naming.param_key(Blog::Post) #=> "post"
293
    #
294
    #   # For shared engine:
295
    #   ActiveModel::Naming.param_key(Blog::Post) #=> "blog_post"
296 297 298 299
    def self.param_key(record_or_class)
      model_name_from_record_or_class(record_or_class).param_key
    end

300 301 302 303 304 305
    def self.model_name_from_record_or_class(record_or_class) #:nodoc:
      if record_or_class.respond_to?(:model_name)
        record_or_class.model_name
      elsif record_or_class.respond_to?(:to_model)
        record_or_class.to_model.class.model_name
      else
306
        record_or_class.class.model_name
307
      end
308 309
    end
    private_class_method :model_name_from_record_or_class
310 311
  end
end