aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/core_ext/object/try.rb
blob: 952fba4541b5ddcd7e8b0f6c5660e5ba53260ab3 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
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
58
59
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
# frozen_string_literal: true
require "delegate"

module ActiveSupport
  module Tryable #:nodoc:
    def try(*a, &b)
      try!(*a, &b) if a.empty? || respond_to?(a.first)
    end

    def try!(*a, &b)
      if a.empty? && block_given?
        if b.arity == 0
          instance_eval(&b)
        else
          yield self
        end
      else
        public_send(*a, &b)
      end
    end
  end
end

class Object
  include ActiveSupport::Tryable

  ##
  # :method: try
  #
  # :call-seq:
  #   try(*a, &b)
  #
  # Invokes the public method whose name goes as first argument just like
  # +public_send+ does, except that if the receiver does not respond to it the
  # call returns +nil+ rather than raising an exception.
  #
  # This method is defined to be able to write
  #
  #   @person.try(:name)
  #
  # instead of
  #
  #   @person.name if @person
  #
  # +try+ calls can be chained:
  #
  #   @person.try(:spouse).try(:name)
  #
  # instead of
  #
  #   @person.spouse.name if @person && @person.spouse
  #
  # +try+ will also return +nil+ if the receiver does not respond to the method:
  #
  #   @person.try(:non_existing_method) # => nil
  #
  # instead of
  #
  #   @person.non_existing_method if @person.respond_to?(:non_existing_method) # => nil
  #
  # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
  # to the method:
  #
  #   nil.try(:to_i) # => nil, rather than 0
  #
  # Arguments and blocks are forwarded to the method if invoked:
  #
  #   @posts.try(:each_slice, 2) do |a, b|
  #     ...
  #   end
  #
  # The number of arguments in the signature must match. If the object responds
  # to the method the call is attempted and +ArgumentError+ is still raised
  # in case of argument mismatch.
  #
  # If +try+ is called without arguments it yields the receiver to a given
  # block unless it is +nil+:
  #
  #   @person.try do |p|
  #     ...
  #   end
  #
  # You can also call try with a block without accepting an argument, and the block
  # will be instance_eval'ed instead:
  #
  #   @person.try { upcase.truncate(50) }
  #
  # Please also note that +try+ is defined on +Object+. Therefore, it won't work
  # with instances of classes that do not have +Object+ among their ancestors,
  # like direct subclasses of +BasicObject+.

  ##
  # :method: try!
  #
  # :call-seq:
  #   try!(*a, &b)
  #
  # Same as #try, but raises a +NoMethodError+ exception if the receiver is
  # not +nil+ and does not implement the tried method.
  #
  #   "a".try!(:upcase) # => "A"
  #   nil.try!(:upcase) # => nil
  #   123.try!(:upcase) # => NoMethodError: undefined method `upcase' for 123:Integer
end

class Delegator
  include ActiveSupport::Tryable

  ##
  # :method: try
  #
  # :call-seq:
  #   try(a*, &b)
  #
  # See Object#try

  ##
  # :method: try!
  #
  # :call-seq:
  #   try!(a*, &b)
  #
  # See Object#try!
end

class NilClass
  # Calling +try+ on +nil+ always returns +nil+.
  # It becomes especially helpful when navigating through associations that may return +nil+.
  #
  #   nil.try(:name) # => nil
  #
  # Without +try+
  #   @person && @person.children.any? && @person.children.first.name
  #
  # With +try+
  #   @person.try(:children).try(:first).try(:name)
  def try(*args)
    nil
  end

  # Calling +try!+ on +nil+ always returns +nil+.
  #
  #   nil.try!(:name) # => nil
  def try!(*args)
    nil
  end
end