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
  #   BookCover.model_name.name   # => "BookCover"
208
  #   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
    #   class Person
    #     include ActiveModel::Model
223 224
    #   end
    #
225
    #   Person.model_name.name     # => "Person"
226 227 228
    #   Person.model_name.class    # => ActiveModel::Name
    #   Person.model_name.singular # => "person"
    #   Person.model_name.plural   # => "people"
J
Joshua Peek 已提交
229
    def model_name
T
thedarkone 已提交
230
      @_model_name ||= begin
231 232 233
        namespace = self.parents.detect do |n|
          n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
        end
T
thedarkone 已提交
234 235
        ActiveModel::Name.new(self, namespace)
      end
J
Joshua Peek 已提交
236
    end
237

238
    # Returns the plural class name of a record or class.
239 240 241 242 243 244 245
    #
    #   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

246
    # Returns the singular class name of a record or class.
247 248 249 250 251 252 253
    #
    #   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

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

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

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

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

301 302 303 304 305 306
    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
307
        record_or_class.class.model_name
308
      end
309 310
    end
    private_class_method :model_name_from_record_or_class
311 312
  end
end