File: api.rb

package info (click to toggle)
ruby-grape 1.6.2-3
  • links: PTS, VCS
  • area: main
  • in suites: bookworm
  • size: 2,156 kB
  • sloc: ruby: 25,265; makefile: 7
file content (201 lines) | stat: -rw-r--r-- 6,859 bytes parent folder | download
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
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# frozen_string_literal: true

require 'grape/router'
require 'grape/api/instance'

module Grape
  # The API class is the primary entry point for creating Grape APIs. Users
  # should subclass this class in order to build an API.
  class API
    # Class methods that we want to call on the API rather than on the API object
    NON_OVERRIDABLE = (Class.new.methods + %i[call call! configuration compile! inherited]).freeze

    class Boolean
      def self.build(val)
        return nil if val != true && val != false

        new
      end
    end

    class Instance
      Boolean = Grape::API::Boolean
    end

    class << self
      attr_accessor :base_instance, :instances

      # Rather than initializing an object of type Grape::API, create an object of type Instance
      def new(*args, &block)
        base_instance.new(*args, &block)
      end

      # When inherited, will create a list of all instances (times the API was mounted)
      # It will listen to the setup required to mount that endpoint, and replicate it on any new instance
      def inherited(api)
        super

        api.initial_setup(Grape::API == self ? Grape::API::Instance : @base_instance)
        api.override_all_methods!
      end

      # Initialize the instance variables on the remountable class, and the base_instance
      # an instance that will be used to create the set up but will not be mounted
      def initial_setup(base_instance_parent)
        @instances = []
        @setup = Set.new
        @base_parent = base_instance_parent
        @base_instance = mount_instance
      end

      # Redefines all methods so that are forwarded to add_setup and be recorded
      def override_all_methods!
        (base_instance.methods - NON_OVERRIDABLE).each do |method_override|
          define_singleton_method(method_override) do |*args, &block|
            add_setup(method_override, *args, &block)
          end
        end
      end

      # Configure an API from the outside. If a block is given, it'll pass a
      # configuration hash to the block which you can use to configure your
      # API. If no block is given, returns the configuration hash.
      # The configuration set here is accessible from inside an API with
      # `configuration` as normal.
      def configure
        config = @base_instance.configuration
        if block_given?
          yield config
          self
        else
          config
        end
      end

      # This is the interface point between Rack and Grape; it accepts a request
      # from Rack and ultimately returns an array of three values: the status,
      # the headers, and the body. See [the rack specification]
      # (http://www.rubydoc.info/github/rack/rack/master/file/SPEC) for more.
      # NOTE: This will only be called on an API directly mounted on RACK
      def call(*args, &block)
        instance_for_rack.call(*args, &block)
      end

      # Alleviates problems with autoloading by tring to search for the constant
      def const_missing(*args)
        if base_instance.const_defined?(*args)
          base_instance.const_get(*args)
        else
          super
        end
      end

      # The remountable class can have a configuration hash to provide some dynamic class-level variables.
      # For instance, a description could be done using: `desc configuration[:description]` if it may vary
      # depending on where the endpoint is mounted. Use with care, if you find yourself using configuration
      # too much, you may actually want to provide a new API rather than remount it.
      def mount_instance(**opts)
        instance = Class.new(@base_parent)
        instance.configuration = Grape::Util::EndpointConfiguration.new(opts[:configuration] || {})
        instance.base = self
        replay_setup_on(instance)
        instance
      end

      # Replays the set up to produce an API as defined in this class, can be called
      # on classes that inherit from Grape::API
      def replay_setup_on(instance)
        @setup.each do |setup_step|
          replay_step_on(instance, setup_step)
        end
      end

      def respond_to?(method, include_private = false)
        super(method, include_private) || base_instance.respond_to?(method, include_private)
      end

      def respond_to_missing?(method, include_private = false)
        base_instance.respond_to?(method, include_private)
      end

      def method_missing(method, *args, &block)
        # If there's a missing method, it may be defined on the base_instance instead.
        if respond_to_missing?(method)
          base_instance.send(method, *args, &block)
        else
          super
        end
      end

      def compile!
        require 'grape/eager_load'
        instance_for_rack.compile! # See API::Instance.compile!
      end

      private

      def instance_for_rack
        if never_mounted?
          base_instance
        else
          mounted_instances.first
        end
      end

      # Adds a new stage to the set up require to get a Grape::API up and running
      def add_setup(method, *args, &block)
        setup_step = { method: method, args: args, block: block }
        @setup += [setup_step]
        last_response = nil
        @instances.each do |instance|
          last_response = replay_step_on(instance, setup_step)
        end
        last_response
      end

      def replay_step_on(instance, setup_step)
        return if skip_immediate_run?(instance, setup_step[:args])

        args = evaluate_arguments(instance.configuration, *setup_step[:args])
        response = instance.send(setup_step[:method], *args, &setup_step[:block])
        if skip_immediate_run?(instance, [response])
          response
        else
          evaluate_arguments(instance.configuration, response).first
        end
      end

      # Skips steps that contain arguments to be lazily executed (on re-mount time)
      def skip_immediate_run?(instance, args)
        instance.base_instance? &&
          (any_lazy?(args) || args.any? { |arg| arg.is_a?(Hash) && any_lazy?(arg.values) })
      end

      def any_lazy?(args)
        args.any? { |argument| argument.respond_to?(:lazy?) && argument.lazy? }
      end

      def evaluate_arguments(configuration, *args)
        args.map do |argument|
          if argument.respond_to?(:lazy?) && argument.lazy?
            argument.evaluate_from(configuration)
          elsif argument.is_a?(Hash)
            argument.transform_values { |value| evaluate_arguments(configuration, value).first }
          elsif argument.is_a?(Array)
            evaluate_arguments(configuration, *argument)
          else
            argument
          end
        end
      end

      def never_mounted?
        mounted_instances.empty?
      end

      def mounted_instances
        instances - [base_instance]
      end
    end
  end
end