Skip to content

Commit 8afa119

Browse files
committed
WIP.
1 parent 919d830 commit 8afa119

17 files changed

+381
-73
lines changed

lib/reynard.rb

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ class Reynard
1515
def_delegators :@specification, :servers
1616
def_delegators :@inflector, :snake_cases
1717

18+
autoload :Coders, 'reynard/coders'
1819
autoload :Content, 'reynard/content'
1920
autoload :Context, 'reynard/context'
2021
autoload :External, 'reynard/external'
@@ -30,7 +31,7 @@ class Reynard
3031
autoload :ResponseContext, 'reynard/response_context'
3132
autoload :RequestContext, 'reynard/request_context'
3233
autoload :Schema, 'reynard/schema'
33-
autoload :Serializer, 'reynard/serializer'
34+
autoload :SerializerSelection, 'reynard/serializer_selection'
3435
autoload :Server, 'reynard/server'
3536
autoload :Specification, 'reynard/specification'
3637
autoload :Template, 'reynard/template'
@@ -56,7 +57,9 @@ def self.user_agent
5657
# Returns supported request body serializers as a Hash-like object keyed on the content-type.
5758
def self.serializers
5859
{
59-
'application/json' => MultiJson
60+
'application/json' => Reynard::Coders::ApplicationJson,
61+
'multipart/form-data' => Reynard::Coders::MultipartFormData,
62+
'text/plain' => Reynard::Coders::TextPlain
6063
}.freeze
6164
end
6265

lib/reynard/coders.rb

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
# frozen_string_literal: true
2+
3+
class Reynard
4+
# Contains built-in (de)serializer classes.
5+
module Coders
6+
autoload :ApplicationJson, 'reynard/coders/application_json'
7+
autoload :MultipartFormData, 'reynard/coders/multipart_form_data'
8+
autoload :TextPlain, 'reynard/coders/text_plain'
9+
end
10+
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
class Reynard
4+
module Coders
5+
# Generates a multi part form data request body.
6+
class ApplicationJson
7+
def initialize(data:)
8+
@data = data
9+
end
10+
11+
def mime_type
12+
'application/json'
13+
end
14+
15+
def headers
16+
{ 'Content-Type' => mime_type.to_s }
17+
end
18+
19+
def body
20+
MultiJson.dump(@data)
21+
end
22+
end
23+
end
24+
end
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
# frozen_string_literal: true
2+
3+
require 'rack/utils'
4+
5+
class Reynard
6+
module Coders
7+
# Generates a multi part form data request body.
8+
class MultipartFormData
9+
# We use all lowercase and no punctiation to be nice to other implementation.
10+
ALPHABET = ('a'..'z').to_a + ('0'..'9').to_a
11+
MULTIPART_BOUNDARY = 0.upto(69).map { ALPHABET.sample }.join.freeze
12+
13+
# Returns the multipart boundary generated for this request body.
14+
attr_reader :multipart_boundary
15+
16+
def initialize(data:)
17+
@data = data
18+
end
19+
20+
def mime_type
21+
'multipart/form-data'
22+
end
23+
24+
def headers
25+
{ 'Content-Type' => %(#{mime_type}; boundary="#{MULTIPART_BOUNDARY}") }
26+
end
27+
28+
def body
29+
body = StringIO.new
30+
@data.each do |name, value|
31+
body << "--#{MULTIPART_BOUNDARY}\r"
32+
case value
33+
when Tempfile, IO
34+
write_io_part(body, name, value)
35+
else
36+
write_part(body, name, value)
37+
end
38+
end
39+
body.string
40+
end
41+
42+
def write_io_part(body, name, io)
43+
filename = self.class.filename(io)
44+
filename = Rack::Utils.escape_path(File.basename(filename)) if filename
45+
content_type = io.respond_to?(:content_type) ? io.content_type : nil
46+
content_length = File.stat(io).size
47+
content_attributes = { 'name' => name, 'filename' => filename }
48+
49+
body << %(Content-Disposition: #{join('form-data', *serialize(content_attributes))}\r)
50+
body << %(Content-Type: #{content_type}\r) if content_type
51+
body << %(Content-Length: #{content_length}\r) if content_length
52+
body << %(\r)
53+
IO.copy_stream(io, body)
54+
body << %(\r)
55+
end
56+
57+
def write_part(body, name, value)
58+
body << %(Content-Disposition: #{join('form-data', *serialize({ 'name' => name }))}\r)
59+
body << %(\r)
60+
body << value
61+
body << %(\r)
62+
end
63+
64+
def join(*items)
65+
items.join('; ')
66+
end
67+
68+
def serialize(attributes)
69+
attributes.compact.flat_map do |name, value|
70+
%(#{name}="#{quote(value)}")
71+
end
72+
end
73+
74+
def quote(name)
75+
name.to_s.gsub('"', '\"')
76+
end
77+
78+
def self.filename(io)
79+
# A lot of IO related types are low level and don't necessarily respond to +respond_to?+, so
80+
# we just yeet the method and hope it sticks.
81+
%i[original_filename filename path].each do |method|
82+
return io.public_send(method)
83+
rescue NoMethodError
84+
end
85+
nil
86+
end
87+
end
88+
end
89+
end

lib/reynard/coders/text_plain.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# frozen_string_literal: true
2+
3+
class Reynard
4+
module Coders
5+
# Generates a plain text request body.
6+
class TextPlain
7+
def initialize(data:)
8+
@data = data
9+
end
10+
11+
def mime_type
12+
'text/plain'
13+
end
14+
15+
def headers
16+
{ 'Content-Type' => mime_type.to_s }
17+
end
18+
19+
def body
20+
@data.to_s
21+
end
22+
end
23+
end
24+
end

lib/reynard/content.rb

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,12 @@ def pick_serializer(serializers)
1414
return unless serializers
1515

1616
@keys.each do |key|
17-
if serializers.key?(key)
18-
return Serializer.new(
19-
content_type: key,
20-
serializer: serializers[key]
21-
)
22-
end
17+
next unless serializers.key?(key)
18+
19+
return SerializerSelection.new(
20+
content_type: key,
21+
serializer_class: serializers[key]
22+
)
2323
end
2424
nil
2525
end

lib/reynard/context.rb

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ def logger(logger)
4848
def serializer(content_type, serializer)
4949
copy(
5050
request: {
51-
serializers: @request_context.serializers.merge({ content_type => serializer })
51+
serializers: @request_context.serializers.merge({ content_type => serializer }).compact
5252
}
5353
)
5454
end
@@ -98,11 +98,11 @@ def copy(request: {}, response: {})
9898
def build_request
9999
Reynard::Http::Request.new(
100100
request_context: @request_context,
101-
serializer: pick_serializer
101+
serializer_selection: serializer_selection
102102
)
103103
end
104104

105-
def pick_serializer
105+
def serializer_selection
106106
@specification
107107
.content(@request_context.operation.node)
108108
.pick_serializer(@request_context.serializers)

lib/reynard/http/request.rb

Lines changed: 31 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ class Http
99
class Request
1010
attr_reader :uri
1111

12-
def initialize(request_context:, serializer:)
12+
def initialize(request_context:, serializer_selection:)
1313
@request_context = request_context
14-
@serializer = serializer
14+
@serializer_selection = serializer_selection
1515
@uri = URI(@request_context.url)
1616
end
1717

@@ -28,22 +28,43 @@ def request_class
2828

2929
def request_headers
3030
{
31-
'User-Agent' => Reynard.user_agent,
32-
'Content-Type' => @serializer&.content_type
33-
}.compact.merge(@request_context.headers || {})
31+
'User-Agent' => Reynard.user_agent
32+
}
33+
.merge(serializer&.headers || {})
34+
.merge(@request_context.headers || {})
3435
end
3536

3637
def build_request
3738
request = request_class.new(uri, request_headers)
38-
if @serializer
39-
@request_context.logger&.debug { @request_context.body }
40-
request.body = @serializer.dump(@request_context.body)
39+
if serializer
40+
write_serializer_body(request)
4141
elsif @request_context.body
42-
@request_context.logger&.debug { @request_context.body }
43-
request.body = @request_context.body
42+
write_serializer_params(request)
4443
end
4544
request
4645
end
46+
47+
def write_serializer_body(request)
48+
@request_context.logger&.debug { @request_context.body }
49+
request.body = serializer.body
50+
end
51+
52+
def write_serializer_params(request)
53+
@request_context.logger&.debug { @request_context.body }
54+
request.body = @request_context.body
55+
end
56+
57+
def serializer
58+
return @serializer if defined?(@serializer)
59+
60+
@serializer = build_serializer
61+
end
62+
63+
def build_serializer
64+
return nil unless @serializer_selection&.serializer_class
65+
66+
@serializer_selection.serializer_class.new(data: @request_context.body)
67+
end
4768
end
4869
end
4970
end

lib/reynard/serializer.rb

Lines changed: 0 additions & 16 deletions
This file was deleted.

lib/reynard/serializer_selection.rb

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# frozen_string_literal: true
2+
3+
class Reynard
4+
# Wraps the choice for a serializer request content type, and returns headers and a serializer
5+
# to generate a request body.
6+
SerializerSelection = Struct.new(
7+
:content_type,
8+
:serializer_class,
9+
keyword_init: true
10+
)
11+
end

0 commit comments

Comments
 (0)