File: plugins.rb

package info (click to toggle)
ruby-sequel 5.63.0-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,408 kB
  • sloc: ruby: 113,747; makefile: 3
file content (165 lines) | stat: -rw-r--r-- 7,042 bytes parent folder | download | duplicates (3)
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
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# frozen-string-literal: true

module Sequel
  # Empty namespace that plugins should use to store themselves,
  # so they can be loaded via Model.plugin.
  #
  # Plugins should be modules with one of the following conditions:
  # * A singleton method named apply, which takes a model, 
  #   additional arguments, and an optional block.  This is called
  #   the first time the plugin is loaded for this model (unless it was
  #   already loaded by an ancestor class), before including/extending
  #   any modules, with the arguments
  #   and block provided to the call to Model.plugin.
  # * A module inside the plugin module named ClassMethods,
  #   which will extend the model class.
  # * A module inside the plugin module named InstanceMethods,
  #   which will be included in the model class.
  # * A module inside the plugin module named DatasetMethods,
  #   which will extend the model's dataset.
  # * A singleton method named configure, which takes a model, 
  #   additional arguments, and an optional block.  This is called
  #   every time the Model.plugin method is called, after including/extending
  #   any modules.
  module Plugins
    # In the given module +mod+, define methods that are call the same method
    # on the dataset.  This is designed for plugins to define dataset methods
    # inside ClassMethods that call the implementations in DatasetMethods.
    #
    # This should not be called with untrusted input or method names that
    # can't be used literally, since it uses class_eval.
    def self.def_dataset_methods(mod, meths)
      Array(meths).each do |meth|
        mod.class_eval("def #{meth}(*args, &block); dataset.#{meth}(*args, &block) end", __FILE__, __LINE__)
        # :nocov:
        mod.send(:ruby2_keywords, meth) if respond_to?(:ruby2_keywords, true)
        # :nocov:
      end
    end

    # Add method to +mod+ that overrides inherited_instance_variables to include the
    # values in this hash.
    def self.inherited_instance_variables(mod, hash)
      mod.send(:define_method, :inherited_instance_variables) do ||
        super().merge!(hash)
      end
      mod.send(:private, :inherited_instance_variables)
    end

    # Add method to +mod+ that overrides set_dataset to call the method afterward.
    def self.after_set_dataset(mod, meth)
      mod.send(:define_method, :set_dataset) do |*a|
        r = super(*a)
        # Allow calling private class methods as methods this specifies are usually private
        send(meth)
        r
      end
    end

    method_num = 0
    method_num_mutex = Mutex.new
    # Return a unique method name symbol for the given suffix.
    SEQUEL_METHOD_NAME = lambda do |suffix|
      :"_sequel_#{suffix}_#{method_num_mutex.synchronize{method_num += 1}}"
    end

    # Define a private instance method using the block with the provided name and
    # expected arity.  If the name is given as a Symbol, it is used directly.
    # If the name is given as a String, a unique name will be generated using
    # that string.  The expected_arity should be either 0 (no arguments) or
    # 1 (single argument).
    #
    # If a block with an arity that does not match the expected arity is used,
    # a deprecation warning will be issued. The method defined should still
    # work, though it will be slower than a method with the expected arity.
    #
    # Sequel only checks arity for regular blocks, not lambdas.  Lambdas were
    # already strict in regards to arity, so there is no need to try to fix
    # arity to keep backwards compatibility for lambdas.
    #
    # Blocks with required keyword arguments are not supported by this method.
    def self.def_sequel_method(model, meth, expected_arity, &block)
      if meth.is_a?(String)
        meth = SEQUEL_METHOD_NAME.call(meth)
      end
      call_meth = meth

      unless block.lambda?
        required_args, optional_args, rest, keyword = _define_sequel_method_arg_numbers(block)

        if keyword == :required
          raise Error, "cannot use block with required keyword arguments when calling define_sequel_method with expected arity #{expected_arity}"
        end

        case expected_arity
        when 0
          unless required_args == 0
            # SEQUEL6: remove
            Sequel::Deprecation.deprecate("Arity mismatch in block passed to define_sequel_method. Expected Arity 0, but arguments required for #{block.inspect}. Support for this will be removed in Sequel 6.")
            b = block
            block = lambda{instance_exec(&b)} # Fallback
          end
        when 1
          if required_args == 0 && optional_args == 0 && !rest
            # SEQUEL6: remove
            Sequel::Deprecation.deprecate("Arity mismatch in block passed to define_sequel_method. Expected Arity 1, but no arguments accepted for #{block.inspect}.  Support for this will be removed in Sequel 6.")
            temp_method = SEQUEL_METHOD_NAME.call("temp")
            model.class_eval("def #{temp_method}(_) #{meth =~ /\A\w+\z/ ? "#{meth}_arity" : "send(:\"#{meth}_arity\")"} end", __FILE__, __LINE__)
            model.send(:alias_method, meth, temp_method)
            model.send(:undef_method, temp_method)
            model.send(:private, meth)
            meth = :"#{meth}_arity"
          elsif required_args > 1
            # SEQUEL6: remove
            Sequel::Deprecation.deprecate("Arity mismatch in block passed to define_sequel_method. Expected Arity 1, but more arguments required for #{block.inspect}.  Support for this will be removed in Sequel 6.")
            b = block
            block = lambda{|r| instance_exec(r, &b)} # Fallback
          end
        else
          raise Error, "unexpected arity passed to define_sequel_method: #{expected_arity.inspect}"
        end
      end

      model.send(:define_method, meth, &block)
      model.send(:private, meth)
      model.send(:alias_method, meth, meth)
      call_meth
    end

    # Return the number of required argument, optional arguments,
    # whether the callable accepts any additional arguments,
    # and whether the callable accepts keyword arguments (true, false
    # or :required).
    def self._define_sequel_method_arg_numbers(callable)
      optional_args = 0
      rest = false
      keyword = false
      callable.parameters.map(&:first).each do |arg_type, _|
        case arg_type
        when :opt
          optional_args += 1
        when :rest
          rest = true
        when :keyreq
          keyword = :required
        when :key, :keyrest
          keyword ||= true
        else
          raise Error, "invalid arg_type passed to _define_sequel_method_arg_numbers: #{arg_type}"
        end
      end
      arity = callable.arity
      if arity < 0
        arity = arity.abs - 1
      end
      required_args = arity
      arity -= 1 if keyword == :required

      # callable currently is always a non-lambda Proc
      optional_args -= arity

      [required_args, optional_args, rest, keyword]
    end
    private_class_method :_define_sequel_method_arg_numbers
  end
end