typesafe-sdk 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +11 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +359 -0
  5. data/lib/typesafe/sdk/answer_parser.rb +36 -0
  6. data/lib/typesafe/sdk/api_error.rb +43 -0
  7. data/lib/typesafe/sdk/api_error_factory.rb +36 -0
  8. data/lib/typesafe/sdk/api_response_validation_error.rb +20 -0
  9. data/lib/typesafe/sdk/api_timeout_error.rb +14 -0
  10. data/lib/typesafe/sdk/body_decoder.rb +18 -0
  11. data/lib/typesafe/sdk/choice.rb +39 -0
  12. data/lib/typesafe/sdk/choice_answer.rb +45 -0
  13. data/lib/typesafe/sdk/client.rb +108 -0
  14. data/lib/typesafe/sdk/configuration.rb +66 -0
  15. data/lib/typesafe/sdk/connection_pool.rb +82 -0
  16. data/lib/typesafe/sdk/error_message.rb +51 -0
  17. data/lib/typesafe/sdk/field_reader.rb +76 -0
  18. data/lib/typesafe/sdk/http_request.rb +30 -0
  19. data/lib/typesafe/sdk/http_response.rb +24 -0
  20. data/lib/typesafe/sdk/invalid_response_field.rb +14 -0
  21. data/lib/typesafe/sdk/json_value.rb +51 -0
  22. data/lib/typesafe/sdk/list_models_response.rb +41 -0
  23. data/lib/typesafe/sdk/model_metadata.rb +39 -0
  24. data/lib/typesafe/sdk/models.rb +37 -0
  25. data/lib/typesafe/sdk/net_http_transport.rb +53 -0
  26. data/lib/typesafe/sdk/noul.rb +49 -0
  27. data/lib/typesafe/sdk/noul_answer.rb +39 -0
  28. data/lib/typesafe/sdk/question_set.rb +55 -0
  29. data/lib/typesafe/sdk/rate_limit_error.rb +14 -0
  30. data/lib/typesafe/sdk/request_builder.rb +58 -0
  31. data/lib/typesafe/sdk/request_logger.rb +68 -0
  32. data/lib/typesafe/sdk/requester.rb +92 -0
  33. data/lib/typesafe/sdk/retry_after.rb +46 -0
  34. data/lib/typesafe/sdk/retry_policy.rb +96 -0
  35. data/lib/typesafe/sdk/score.rb +40 -0
  36. data/lib/typesafe/sdk/score_answer.rb +53 -0
  37. data/lib/typesafe/sdk/stderr_logger.rb +42 -0
  38. data/lib/typesafe/sdk/system_one_response.rb +48 -0
  39. data/lib/typesafe/sdk/usage.rb +37 -0
  40. data/lib/typesafe/sdk/version.rb +7 -0
  41. data/lib/typesafe/sdk.rb +47 -0
  42. metadata +108 -0
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class Client
6
+ attr_reader :retry_policy
7
+
8
+ class << self
9
+ def open(**options)
10
+ client = new(**options)
11
+ return client unless block_given?
12
+
13
+ begin
14
+ yield(client)
15
+ ensure
16
+ client.close
17
+ end
18
+ end
19
+ end
20
+
21
+ def initialize(
22
+ api_key:,
23
+ model: nil,
24
+ base_url: nil,
25
+ timeout: nil,
26
+ headers: nil,
27
+ user_agent: nil,
28
+ retry_policy: nil,
29
+ logger: nil,
30
+ transport: nil
31
+ )
32
+ @configuration = Configuration.resolve(
33
+ api_key: api_key,
34
+ base_url: base_url,
35
+ model: model,
36
+ timeout: timeout,
37
+ headers: headers,
38
+ user_agent: user_agent,
39
+ logger: logger
40
+ )
41
+ @retry_policy = retry_policy || RetryPolicy.new
42
+ @transport = transport || NetHttpTransport.new
43
+ @requester = Requester.new(transport: @transport, logger: @configuration.logger)
44
+ @models = Models.new(configuration: @configuration, requester: @requester, retry_policy: @retry_policy)
45
+ end
46
+
47
+ attr_reader :models
48
+
49
+ def model
50
+ configuration.model
51
+ end
52
+
53
+ def base_url
54
+ configuration.base_url
55
+ end
56
+
57
+ def timeout
58
+ configuration.timeout
59
+ end
60
+
61
+ def user_agent
62
+ configuration.user_agent
63
+ end
64
+
65
+ def system_one(state:, questions:, model: nil, retry_policy: nil, timeout: nil, extra_headers: nil, extra_body: nil)
66
+ request = RequestBuilder.build(
67
+ configuration: configuration,
68
+ http_method: "POST",
69
+ path: SYSTEM_ONE_PATH,
70
+ body: system_one_body(state: state, questions: questions, model: model, extra_body: extra_body),
71
+ timeout: timeout,
72
+ headers: extra_headers
73
+ )
74
+ requester.perform(
75
+ request: request,
76
+ response_class: SystemOneResponse,
77
+ retry_policy: retry_policy || self.retry_policy
78
+ )
79
+ end
80
+
81
+ def close
82
+ transport.close if transport.respond_to?(:close)
83
+ nil
84
+ end
85
+
86
+ def inspect
87
+ "#<#{self.class.name} base_url=#{base_url.inspect} model=#{model.inspect} timeout=#{timeout.inspect}>"
88
+ end
89
+
90
+ private
91
+
92
+ attr_reader :configuration, :transport, :requester
93
+
94
+ def system_one_body(state:, questions:, model:, extra_body:)
95
+ raise(Error, "state must be a string, hash, or array") unless JsonValue.content?(state)
96
+
97
+ body = {
98
+ "state" => JsonValue.normalize(state, path: "state"),
99
+ "model" => model.nil? ? configuration.model : model,
100
+ "questions" => QuestionSet.normalize(questions)
101
+ }
102
+ return body if extra_body.nil?
103
+
104
+ body.merge(JsonValue.normalize(extra_body.to_h, path: "extra_body"))
105
+ end
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class Configuration
6
+ attr_reader :base_url, :model, :timeout, :headers, :user_agent, :logger
7
+
8
+ class << self
9
+ def resolve(api_key:, base_url: nil, model: nil, timeout: nil, headers: nil, user_agent: nil, logger: nil)
10
+ raise(Error, "api_key is required") unless api_key.is_a?(String) && !api_key.strip.empty?
11
+
12
+ normalized_headers = (headers || {}).to_h { |name, value| [name.to_s, value.to_s] }
13
+ header_user_agent = normalized_headers.find { |name, _value| name.casecmp?("User-Agent") }
14
+ normalized_headers.delete_if { |name, _value| name.casecmp?("User-Agent") }
15
+
16
+ new(
17
+ api_key: api_key,
18
+ base_url: base_url.nil? ? DEFAULT_BASE_URL : base_url,
19
+ model: model.nil? ? DEFAULT_MODEL : model,
20
+ timeout: validate_timeout(timeout.nil? ? DEFAULT_TIMEOUT : timeout),
21
+ headers: normalized_headers,
22
+ user_agent: validate_user_agent(user_agent || (header_user_agent && header_user_agent.last) || DEFAULT_USER_AGENT),
23
+ logger: logger
24
+ )
25
+ end
26
+
27
+ def validate_timeout(value)
28
+ return value if value.is_a?(Numeric) && value.finite? && value.positive?
29
+
30
+ raise(Error, "timeout must be a positive, finite number of seconds")
31
+ end
32
+
33
+ private
34
+
35
+ def validate_user_agent(value)
36
+ return value if value.is_a?(String) && !value.strip.empty?
37
+
38
+ raise(Error, "user_agent must be a nonempty string")
39
+ end
40
+ end
41
+
42
+ def initialize(api_key:, base_url:, model:, timeout:, headers:, user_agent:, logger:)
43
+ @api_key = api_key
44
+ @base_url = base_url.sub(%r{/+\z}, "")
45
+ @model = model
46
+ @timeout = timeout
47
+ @headers = headers.freeze
48
+ @user_agent = user_agent
49
+ @logger = logger
50
+ freeze
51
+ end
52
+
53
+ def authorization
54
+ "Bearer #{api_key}"
55
+ end
56
+
57
+ def inspect
58
+ "#<#{self.class.name} base_url=#{base_url.inspect} model=#{model.inspect} timeout=#{timeout.inspect}>"
59
+ end
60
+
61
+ private
62
+
63
+ attr_reader :api_key
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class ConnectionPool
6
+ def initialize
7
+ @mutex = Mutex.new
8
+ @idle = {}
9
+ @pid = Process.pid
10
+ end
11
+
12
+ def checkout(uri:, timeout:)
13
+ connection = mutex.synchronize do
14
+ reset_after_fork
15
+ idle_for(key_for(uri)).pop
16
+ end
17
+ connection = start(uri: uri, timeout: timeout) if connection.nil?
18
+ apply_timeout(connection: connection, timeout: timeout)
19
+ connection
20
+ end
21
+
22
+ def checkin(uri:, connection:)
23
+ return discard(connection) unless connection.started?
24
+
25
+ mutex.synchronize do
26
+ reset_after_fork
27
+ idle_for(key_for(uri)).push(connection)
28
+ end
29
+ end
30
+
31
+ def discard(connection)
32
+ return if connection.nil?
33
+
34
+ connection.finish if connection.started?
35
+ rescue IOError, SystemCallError, OpenSSL::SSL::SSLError
36
+ nil
37
+ end
38
+
39
+ def shutdown
40
+ connections = mutex.synchronize do
41
+ all = idle.values.flatten
42
+ idle.clear
43
+ all
44
+ end
45
+ connections.each { |connection| discard(connection) }
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :mutex, :idle
51
+
52
+ def key_for(uri)
53
+ "#{uri.scheme}://#{uri.host}:#{uri.port}"
54
+ end
55
+
56
+ def idle_for(key)
57
+ idle[key] ||= []
58
+ end
59
+
60
+ def reset_after_fork
61
+ return if @pid == Process.pid
62
+
63
+ @idle = {}
64
+ @pid = Process.pid
65
+ end
66
+
67
+ def start(uri:, timeout:)
68
+ connection = Net::HTTP.new(uri.host, uri.port)
69
+ connection.use_ssl = uri.scheme == "https"
70
+ apply_timeout(connection: connection, timeout: timeout)
71
+ connection.start
72
+ end
73
+
74
+ def apply_timeout(connection:, timeout:)
75
+ connection.open_timeout = timeout
76
+ connection.read_timeout = timeout
77
+ connection.write_timeout = timeout
78
+ connection.ssl_timeout = timeout
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class ErrorMessage
6
+ class << self
7
+ def extract(body)
8
+ return presence(body) if body.is_a?(String)
9
+ return unless body.is_a?(Hash)
10
+
11
+ from_error(body["error"]) ||
12
+ string_or_nil(body["message"]) ||
13
+ from_detail(body["detail"])
14
+ end
15
+
16
+ private
17
+
18
+ def from_error(error)
19
+ return error if error.is_a?(String)
20
+ return unless error.is_a?(Hash)
21
+
22
+ string_or_nil(error["message"])
23
+ end
24
+
25
+ def from_detail(detail)
26
+ return detail if detail.is_a?(String)
27
+ return string_or_nil(detail["message"]) if detail.is_a?(Hash)
28
+ return unless detail.is_a?(Array)
29
+
30
+ presence(detail.filter_map { |entry| validation_entry(entry) }.join("; "))
31
+ end
32
+
33
+ def validation_entry(entry)
34
+ return unless entry.is_a?(Hash) && entry["msg"].is_a?(String)
35
+
36
+ location = entry["loc"]
37
+ path = location.is_a?(Array) ? location.reject { |item| item == "body" }.join(".") : ""
38
+ path.empty? ? entry["msg"] : "#{path}: #{entry["msg"]}"
39
+ end
40
+
41
+ def string_or_nil(value)
42
+ value.is_a?(String) ? value : nil
43
+ end
44
+
45
+ def presence(value)
46
+ value.empty? ? nil : value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class FieldReader
6
+ INTEGER_KEY = /\A-?\d+\z/
7
+
8
+ class << self
9
+ def object(value, path:)
10
+ return value if value.is_a?(Hash)
11
+
12
+ raise(InvalidResponseField, path)
13
+ end
14
+
15
+ def fetch(hash:, key:, path:)
16
+ raise(InvalidResponseField, join(path: path, key: key)) unless hash.key?(key)
17
+
18
+ hash[key]
19
+ end
20
+
21
+ def string(hash:, key:, path:)
22
+ value = fetch(hash: hash, key: key, path: path)
23
+ return value if value.is_a?(String)
24
+
25
+ raise(InvalidResponseField, join(path: path, key: key))
26
+ end
27
+
28
+ def number(hash:, key:, path:)
29
+ value = fetch(hash: hash, key: key, path: path)
30
+ return value.to_f if value.is_a?(Numeric)
31
+
32
+ raise(InvalidResponseField, join(path: path, key: key))
33
+ end
34
+
35
+ def optional_integer(hash:, key:, path:)
36
+ value = hash[key]
37
+ return value if value.nil? || value.is_a?(Integer)
38
+
39
+ raise(InvalidResponseField, join(path: path, key: key))
40
+ end
41
+
42
+ def number_map(hash:, key:, path:)
43
+ map_path = join(path: path, key: key)
44
+ object(fetch(hash: hash, key: key, path: path), path: map_path).to_h do |name, value|
45
+ raise(InvalidResponseField, join(path: map_path, key: name)) unless value.is_a?(Numeric)
46
+
47
+ [name, value.to_f]
48
+ end
49
+ end
50
+
51
+ def content_map(hash:, key:, path:)
52
+ map_path = join(path: path, key: key)
53
+ map = object(fetch(hash: hash, key: key, path: path), path: map_path)
54
+ map.each do |name, value|
55
+ next if value.is_a?(String) || value.is_a?(Hash) || value.is_a?(Array)
56
+
57
+ raise(InvalidResponseField, join(path: map_path, key: name))
58
+ end
59
+ map
60
+ end
61
+
62
+ def integer_keys(hash:, path:)
63
+ hash.to_h do |key, value|
64
+ raise(InvalidResponseField, join(path: path, key: key)) unless INTEGER_KEY.match?(key)
65
+
66
+ [Integer(key, 10), value]
67
+ end
68
+ end
69
+
70
+ def join(path:, key:)
71
+ path.empty? ? key.to_s : "#{path}.#{key}"
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class HTTPRequest
6
+ attr_reader :http_method, :url, :headers, :body, :timeout
7
+
8
+ def initialize(http_method:, url:, headers:, body:, timeout:)
9
+ @http_method = http_method
10
+ @url = url
11
+ @headers = headers.freeze
12
+ @body = body
13
+ @timeout = timeout
14
+ freeze
15
+ end
16
+
17
+ def with_headers(headers)
18
+ self.class.new(http_method: http_method, url: url, headers: headers, body: body, timeout: timeout)
19
+ end
20
+
21
+ def endpoint
22
+ uri = URI.parse(url)
23
+ port = uri.port == uri.default_port ? "" : ":#{uri.port}"
24
+ "#{http_method} #{uri.scheme}://#{uri.host}#{port}#{uri.path}"
25
+ rescue URI::InvalidURIError
26
+ http_method
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class HTTPResponse
6
+ attr_reader :status, :headers, :body
7
+
8
+ def initialize(status:, headers:, body:)
9
+ @status = status
10
+ @headers = headers.to_h { |name, value| [name.to_s.downcase, value] }.freeze
11
+ @body = body.to_s
12
+ freeze
13
+ end
14
+
15
+ def success?
16
+ status.between?(200, 299)
17
+ end
18
+
19
+ def json
20
+ JSON.parse(body)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class InvalidResponseField < StandardError
6
+ attr_reader :field_path
7
+
8
+ def initialize(field_path)
9
+ @field_path = field_path
10
+ super("invalid response data at #{field_path.inspect}")
11
+ end
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class JsonValue
6
+ class << self
7
+ def normalize(value, path: "value")
8
+ case value
9
+ when nil, true, false, String, Integer then value
10
+ when Float then normalize_float(value: value, path: path)
11
+ when Symbol then value.to_s
12
+ when Hash then normalize_hash(value: value, path: path)
13
+ when Array then value.each_with_index.map { |item, index| normalize(item, path: "#{path}.#{index}") }
14
+ else normalize_object(value: value, path: path)
15
+ end
16
+ end
17
+
18
+ def content?(value)
19
+ value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(Hash) || value.is_a?(Array)
20
+ end
21
+
22
+ private
23
+
24
+ def normalize_float(value:, path:)
25
+ return value if value.finite?
26
+
27
+ raise(Error, "#{path} is #{value}, which cannot be encoded as json")
28
+ end
29
+
30
+ def normalize_hash(value:, path:)
31
+ value.each_with_object({}) do |(key, item), result|
32
+ unless key.is_a?(String) || key.is_a?(Symbol)
33
+ raise(Error, "#{path} has a #{key.class} key, json object keys must be strings or symbols")
34
+ end
35
+
36
+ result[key.to_s] = normalize(item, path: "#{path}.#{key}")
37
+ end
38
+ end
39
+
40
+ def normalize_object(value:, path:)
41
+ if value.respond_to?(:as_json)
42
+ converted = value.as_json
43
+ return normalize(converted, path: path) unless converted.equal?(value)
44
+ end
45
+
46
+ raise(Error, "#{path} is a #{value.class}, which cannot be encoded as json")
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class ListModelsResponse
6
+ include Enumerable
7
+
8
+ attr_reader :models, :http_response
9
+
10
+ class << self
11
+ def from_body(body, request_logger: nil, http_response: nil)
12
+ root = FieldReader.object(body, path: "")
13
+ entries = FieldReader.fetch(hash: root, key: "models", path: "")
14
+ raise(InvalidResponseField, "models") unless entries.is_a?(Array)
15
+
16
+ models = entries.each_with_index.map do |entry, index|
17
+ path = "models.#{index}"
18
+ ModelMetadata.from_hash(FieldReader.object(entry, path: path), path: path)
19
+ end
20
+ new(models: models, http_response: http_response)
21
+ end
22
+ end
23
+
24
+ def initialize(models:, http_response: nil)
25
+ @models = models.freeze
26
+ @http_response = http_response
27
+ freeze
28
+ end
29
+
30
+ def each(&block)
31
+ models.each(&block)
32
+ end
33
+
34
+ def request_id
35
+ return unless http_response
36
+
37
+ http_response.headers[REQUEST_ID_HEADER]
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class ModelMetadata
6
+ attr_reader :name, :description, :release_date
7
+
8
+ class << self
9
+ def from_hash(hash, path:)
10
+ new(
11
+ name: FieldReader.string(hash: hash, key: "name", path: path),
12
+ description: FieldReader.string(hash: hash, key: "description", path: path),
13
+ release_date: FieldReader.string(hash: hash, key: "release_date", path: path)
14
+ )
15
+ end
16
+ end
17
+
18
+ def initialize(name:, description:, release_date:)
19
+ @name = name
20
+ @description = description
21
+ @release_date = release_date
22
+ freeze
23
+ end
24
+
25
+ def to_h
26
+ { name: name, description: description, release_date: release_date }
27
+ end
28
+
29
+ def ==(other)
30
+ other.is_a?(self.class) && other.to_h == to_h
31
+ end
32
+ alias eql? ==
33
+
34
+ def hash
35
+ [self.class, to_h].hash
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class Models
6
+ def initialize(configuration:, requester:, retry_policy:)
7
+ @configuration = configuration
8
+ @requester = requester
9
+ @default_retry_policy = retry_policy
10
+ end
11
+
12
+ def list(retry_policy: nil, timeout: nil, extra_headers: nil)
13
+ request = RequestBuilder.build(
14
+ configuration: configuration,
15
+ http_method: "GET",
16
+ path: MODELS_PATH,
17
+ body: nil,
18
+ timeout: timeout,
19
+ headers: extra_headers
20
+ )
21
+ requester.perform(
22
+ request: request,
23
+ response_class: ListModelsResponse,
24
+ retry_policy: retry_policy || default_retry_policy
25
+ )
26
+ end
27
+
28
+ def inspect
29
+ "#<#{self.class.name}>"
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :configuration, :requester, :default_retry_policy
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Typesafe
4
+ module SDK
5
+ class NetHttpTransport
6
+ REQUEST_CLASSES = { "GET" => Net::HTTP::Get, "POST" => Net::HTTP::Post }.freeze
7
+ CONNECTION_ERRORS = [
8
+ SocketError,
9
+ SystemCallError,
10
+ IOError,
11
+ OpenSSL::SSL::SSLError,
12
+ Net::HTTPBadResponse,
13
+ Net::ProtocolError,
14
+ Zlib::Error
15
+ ].freeze
16
+
17
+ def initialize(pool: ConnectionPool.new)
18
+ @pool = pool
19
+ end
20
+
21
+ def call(request)
22
+ uri = URI.parse(request.url)
23
+ connection = nil
24
+ connection = pool.checkout(uri: uri, timeout: request.timeout)
25
+ raw = connection.request(build(uri: uri, request: request))
26
+ pool.checkin(uri: uri, connection: connection)
27
+ HTTPResponse.new(status: raw.code.to_i, headers: raw.each_header.to_h, body: raw.body)
28
+ rescue Timeout::Error
29
+ pool.discard(connection)
30
+ raise(APITimeoutError.new(timeout: request.timeout))
31
+ rescue *CONNECTION_ERRORS => e
32
+ pool.discard(connection)
33
+ raise(APIConnectionError, "connection error: #{e.class}: #{e.message}")
34
+ end
35
+
36
+ def close
37
+ pool.shutdown
38
+ end
39
+
40
+ private
41
+
42
+ attr_reader :pool
43
+
44
+ def build(uri:, request:)
45
+ request_class = REQUEST_CLASSES.fetch(request.http_method)
46
+ net_request = request_class.new(uri.request_uri)
47
+ request.headers.each { |name, value| net_request[name] = value }
48
+ net_request.body = request.body unless request.body.nil?
49
+ net_request
50
+ end
51
+ end
52
+ end
53
+ end