naming.rb 9.6 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
  module Naming
217 218 219 220 221 222
    def self.extended(base) #:nodoc:
      base.class_eval do
        delegate :model_name, to: :class
      end
    end

J
Joshua Peek 已提交
223
    # Returns an ActiveModel::Name object for module. It can be
224 225
    # used to retrieve all kinds of naming-related information
    # (See ActiveModel::Name for more information).
226
    #
227 228
    #   class Person
    #     include ActiveModel::Model
229 230
    #   end
    #
231
    #   Person.model_name.name     # => "Person"
232 233 234
    #   Person.model_name.class    # => ActiveModel::Name
    #   Person.model_name.singular # => "person"
    #   Person.model_name.plural   # => "people"
J
Joshua Peek 已提交
235
    def model_name
T
thedarkone 已提交
236
      @_model_name ||= begin
237 238 239
        namespace = self.parents.detect do |n|
          n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
        end
T
thedarkone 已提交
240 241
        ActiveModel::Name.new(self, namespace)
      end
J
Joshua Peek 已提交
242
    end
243

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

252
    # Returns the singular class name of a record or class.
253 254 255 256 257 258 259
    #
    #   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

260
    # Identifies whether the class name of a record or class is uncountable.
261 262
    #
    #   ActiveModel::Naming.uncountable?(Sheep) # => true
263
    #   ActiveModel::Naming.uncountable?(Post)  # => false
264 265 266 267
    def self.uncountable?(record_or_class)
      plural(record_or_class) == singular(record_or_class)
    end

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

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

295 296 297
    # Returns string to use for params names. It differs for
    # namespaced models regarding whether it's inside isolated engine.
    #
298
    #   # For isolated engine:
299
    #   ActiveModel::Naming.param_key(Blog::Post) # => "post"
300
    #
301
    #   # For shared engine:
302
    #   ActiveModel::Naming.param_key(Blog::Post) # => "blog_post"
303 304 305 306
    def self.param_key(record_or_class)
      model_name_from_record_or_class(record_or_class).param_key
    end

307 308 309 310
    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)
311
        record_or_class.to_model.model_name
312
      else
313
        record_or_class.class.model_name
314
      end
315 316
    end
    private_class_method :model_name_from_record_or_class
317 318
  end
end