Class: Explicit::Request

Inherits:
Object
  • Object
show all
Defined in:
lib/explicit/request/route.rb,
lib/explicit/request/example.rb,
lib/explicit/request/response.rb,
lib/explicit/request.rb

Defined Under Namespace

Classes: InvalidParamsError, InvalidResponseError

Constant Summary collapse

Route =
::Data.define(:method, :path) do
  def to_s
    "#{method.to_s.upcase} #{path}"
  end

  def params
    path.split("/").filter_map do |part|
      part[1..-1].to_sym if part.start_with?(":")
    end
  end

  def accepts_request_body?
    method == :post || method == :put || method == :patch
  end

  def replace_path_params(values)
    values.reduce(path) do |acc_path, (key, value)|
      acc_path.gsub(":#{key}", value.to_s)
    end
  end

  def path_with_curly_syntax
    params.reduce(path) do |acc_path, param|
      acc_path.gsub(":#{param}", "{#{param}}")
    end
  end
end
Example =
::Data.define(:request, :params, :headers, :response) do
  def to_curl_lines
    route = request.routes.first
    method = route.method.to_s.upcase
    path = route.replace_path_params(params)
    body_params = params.slice(*request.params_type.body_params_type.attributes.keys)
    query_params = params.slice(*request.params_type.query_params_type.attributes.keys)

    url = "#{request.get_base_url}#{request.get_base_path}#{path}#{query_params.present? ? "?#{query_params.to_query}" : ""}"
    curl_request = "curl -X#{method} \"#{url}\""

    curl_headers =
      if body_params.empty?
        []
      elsif request.accepts_file_upload?
        ['-H "Content-Type: multipart/form-data"']
      else
        ['-H "Content-Type: application/json"']
      end

    headers.each do |key, value|
      curl_headers << "-H \"#{key}: #{value}\""
    end

    curl_body =
      if body_params.empty?
        []
      elsif request.accepts_file_upload?
        file_params = request.params_type.attributes.filter do |name, type|
          type.is_a?(Explicit::Type::File)
        end.to_h

        non_file_params = body_params.except(*file_params.keys)

        curl_non_file_params = non_file_params.to_query.split("&").map do |key_value|
          "-F \"#{CGI.unescape(key_value).gsub('"', '\"')}\""
        end

        curl_file_params = file_params.map do |name, _|
          "-F #{name}=\"#{body_params[name]}\""
        end

        curl_non_file_params.concat(curl_file_params)
      else
        # https://stackoverflow.com/questions/34847981/curl-with-multiline-of-json
        ["-d @- << EOF\n#{JSON.pretty_generate(body_params)}\nEOF"]
      end

    [curl_request].concat(curl_headers).concat(curl_body)
  end
end
Response =
::Data.define(:status, :data) do
  def dig(...) = data.dig(...)
end

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(&block) ⇒ Request

Returns a new instance of Request.



6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/explicit/request.rb', line 6

def initialize(&block)
  @routes = []
  @headers = {}
  @params = {}
  @responses = Hash.new { |hash, key| hash[key] = [] }
  @examples = Hash.new { |hash, key| hash[key] = [] }

  instance_eval(&block)

  define_missing_path_params!

  if Explicit.configuration.rescue_from_invalid_params? && @params.any?
    @responses[422] << {
      error: "invalid_params",
      params: [
        :description,
        "An object containing error messages for all invalid params",
        [ :hash, :string, :string ]
      ]
    }
  end
end

Instance Attribute Details

#examplesObject (readonly)

Returns the value of attribute examples.



4
5
6
# File 'lib/explicit/request.rb', line 4

def examples
  @examples
end

#headersObject (readonly)

Returns the value of attribute headers.



4
5
6
# File 'lib/explicit/request.rb', line 4

def headers
  @headers
end

#paramsObject (readonly)

Returns the value of attribute params.



4
5
6
# File 'lib/explicit/request.rb', line 4

def params
  @params
end

#responsesObject (readonly)

Returns the value of attribute responses.



4
5
6
# File 'lib/explicit/request.rb', line 4

def responses
  @responses
end

#routesObject (readonly)

Returns the value of attribute routes.



4
5
6
# File 'lib/explicit/request.rb', line 4

def routes
  @routes
end

Instance Method Details

#accepts_file_upload?Boolean

Returns:

  • (Boolean)


179
180
181
182
183
# File 'lib/explicit/request.rb', line 179

def accepts_file_upload?
  params_type.attributes.any? do |name, type|
    type.is_a?(Explicit::Type::File)
  end
end

#base_path(prefix) ⇒ Object



62
# File 'lib/explicit/request.rb', line 62

def base_path(prefix) = (@base_path = prefix)

#base_url(url) ⇒ Object



59
# File 'lib/explicit/request.rb', line 59

def base_url(url) = (@base_url = url)

#custom_authorization_format?Boolean

Returns:

  • (Boolean)


197
198
199
# File 'lib/explicit/request.rb', line 197

def custom_authorization_format?
  @headers.key?("Authorization") && !requires_basic_authorization? && !requires_bearer_authorization?
end

#delete(path) ⇒ Object



55
# File 'lib/explicit/request.rb', line 55

def delete(path)  = @routes << Route.new(method: :delete, path:)

#description(markdown) ⇒ Object



68
# File 'lib/explicit/request.rb', line 68

def description(markdown) = (@description = markdown)

#example(params:, response:, headers: {}) ⇒ Object Also known as: add_example

Raises:

  • (ArgumentError)


144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
# File 'lib/explicit/request.rb', line 144

def example(params:, response:, headers: {})
  raise ArgumentError.new("missing :status in response") if !response.key?(:status)
  raise ArgumentError.new("missing :data in response")   if !response.key?(:data)

  status, data = response.values_at(:status, :data)

  response = Response.new(status:, data:)

  case responses_type(status:).validate(data)
  in [:ok, _]
    nil

  in [:error, err]
    if ::Explicit.configuration.raise_on_invalid_example?
      raise InvalidResponseError.new(response, err)
    else
      ::Rails.logger.error("[Explicit] Invalid response for #{gid} with status #{status}: #{err}")
    end
  end

  @examples[response.status] << Example.new(request: self, params:, headers:, response:)
end

#get(path) ⇒ Object



51
# File 'lib/explicit/request.rb', line 51

def get(path)     = @routes << Route.new(method: :get, path:)

#get_base_pathObject



63
# File 'lib/explicit/request.rb', line 63

def get_base_path = @base_path

#get_base_urlObject



60
# File 'lib/explicit/request.rb', line 60

def get_base_url = @base_url

#get_descriptionObject



69
# File 'lib/explicit/request.rb', line 69

def get_description = @description

#get_titleObject



66
# File 'lib/explicit/request.rb', line 66

def get_title = @title || @routes.first.to_s

#gidObject



175
176
177
# File 'lib/explicit/request.rb', line 175

def gid
  routes.first.to_s
end

#head(path) ⇒ Object



52
# File 'lib/explicit/request.rb', line 52

def head(path)    = @routes << Route.new(method: :head, path:)

#header(name, type, **options) ⇒ Object



104
105
106
107
108
109
110
111
112
# File 'lib/explicit/request.rb', line 104

def header(name, type,  **options)
  raise ArgumentError("duplicated header #{name}") if @headers.key?(name)

  if (auth_type = options[:auth])
    type = [ :_auth_type, auth_type, type ]
  end

  @headers[name] = type
end

#headers_typeObject



185
186
187
# File 'lib/explicit/request.rb', line 185

def headers_type
  @headers_type ||= Explicit::Type.build(@headers)
end

#new(&block) ⇒ Object



29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
# File 'lib/explicit/request.rb', line 29

def new(&block)
  this = self

  self.class.new do
    instance_variable_set(:@base_url,  this.get_base_url)
    instance_variable_set(:@base_path, this.get_base_path)
    instance_variable_set(:@routes,    this.routes.dup)
    instance_variable_set(:@headers,   this.headers.dup)
    instance_variable_set(:@params,    this.params.dup)

    this.responses.each do |status, types|
      @responses[status] = types.dup
    end

    this.examples.each do |status, examples|
      @examples[status] = examples.dup
    end

    instance_eval(&block)
  end
end

#options(path) ⇒ Object



56
# File 'lib/explicit/request.rb', line 56

def options(path) = @routes << Route.new(method: :options, path:)

#param(name, type, **options) ⇒ Object



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
# File 'lib/explicit/request.rb', line 114

def param(name, type, **options)
  raise ArgumentError("duplicated param #{name}") if @params.key?(name)

  if options[:optional]
    type = [ :nilable, type ]
  end

  if (defaultval = options[:default])
    type = [ :default, defaultval, type ]
  end

  if (description = options[:description])
    type = [ :description, description, type ]
  end

  if @routes.first&.params&.include?(name)
    type = [ :_param_location, :path, type ]
  elsif @routes.first&.accepts_request_body?
    type = [ :_param_location, :body, type ]
  else
    type = [ :_param_location, :query, type ]
  end

  @params[name] = type
end

#params_typeObject



189
190
191
# File 'lib/explicit/request.rb', line 189

def params_type
  @params_type ||= Explicit::Type.build(@params)
end

#patch(path) ⇒ Object



57
# File 'lib/explicit/request.rb', line 57

def patch(path)   = @routes << Route.new(method: :patch, path:)

#post(path) ⇒ Object



53
# File 'lib/explicit/request.rb', line 53

def post(path)    = @routes << Route.new(method: :post, path:)

#put(path) ⇒ Object



54
# File 'lib/explicit/request.rb', line 54

def put(path)     = @routes << Route.new(method: :put, path:)

#requires_basic_authorization?Boolean

Returns:

  • (Boolean)


201
202
203
204
205
# File 'lib/explicit/request.rb', line 201

def requires_basic_authorization?
  authorization = headers_type.attributes["Authorization"]

  authorization&.auth_basic? || authorization&.format&.to_s&.include?("Basic")
end

#requires_bearer_authorization?Boolean

Returns:

  • (Boolean)


207
208
209
210
211
# File 'lib/explicit/request.rb', line 207

def requires_bearer_authorization?
  authorization = headers_type.attributes["Authorization"]

  authorization&.auth_bearer? || authorization&.format&.to_s&.include?("Bearer")
end

#response(status, typespec) ⇒ Object



140
141
142
# File 'lib/explicit/request.rb', line 140

def response(status, typespec)
  @responses[status] << typespec
end

#responses_type(status:) ⇒ Object



193
194
195
# File 'lib/explicit/request.rb', line 193

def responses_type(status:)
  Explicit::Type.build([ :one_of, *@responses[status] ])
end

#title(text) ⇒ Object



65
# File 'lib/explicit/request.rb', line 65

def title(text) = (@title = text)

#validate!(values) ⇒ Object



168
169
170
171
172
173
# File 'lib/explicit/request.rb', line 168

def validate!(values)
  case params_type.validate(values)
  in [:ok, validated_data] then validated_data
  in [:error, err] then raise InvalidParamsError.new(err)
  end
end