Skip to content

Commit

Permalink
WIP.
Browse files Browse the repository at this point in the history
  • Loading branch information
Manfred committed May 13, 2024
1 parent 919d830 commit 8afa119
Show file tree
Hide file tree
Showing 17 changed files with 381 additions and 73 deletions.
7 changes: 5 additions & 2 deletions lib/reynard.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ class Reynard
def_delegators :@specification, :servers
def_delegators :@inflector, :snake_cases

autoload :Coders, 'reynard/coders'
autoload :Content, 'reynard/content'
autoload :Context, 'reynard/context'
autoload :External, 'reynard/external'
Expand All @@ -30,7 +31,7 @@ class Reynard
autoload :ResponseContext, 'reynard/response_context'
autoload :RequestContext, 'reynard/request_context'
autoload :Schema, 'reynard/schema'
autoload :Serializer, 'reynard/serializer'
autoload :SerializerSelection, 'reynard/serializer_selection'
autoload :Server, 'reynard/server'
autoload :Specification, 'reynard/specification'
autoload :Template, 'reynard/template'
Expand All @@ -56,7 +57,9 @@ def self.user_agent
# Returns supported request body serializers as a Hash-like object keyed on the content-type.
def self.serializers
{
'application/json' => MultiJson
'application/json' => Reynard::Coders::ApplicationJson,
'multipart/form-data' => Reynard::Coders::MultipartFormData,
'text/plain' => Reynard::Coders::TextPlain
}.freeze
end

Expand Down
10 changes: 10 additions & 0 deletions lib/reynard/coders.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# frozen_string_literal: true

class Reynard
# Contains built-in (de)serializer classes.
module Coders
autoload :ApplicationJson, 'reynard/coders/application_json'
autoload :MultipartFormData, 'reynard/coders/multipart_form_data'
autoload :TextPlain, 'reynard/coders/text_plain'
end
end
24 changes: 24 additions & 0 deletions lib/reynard/coders/application_json.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

class Reynard
module Coders
# Generates a multi part form data request body.
class ApplicationJson
def initialize(data:)
@data = data
end

def mime_type
'application/json'
end

def headers
{ 'Content-Type' => mime_type.to_s }
end

def body
MultiJson.dump(@data)
end
end
end
end
89 changes: 89 additions & 0 deletions lib/reynard/coders/multipart_form_data.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# frozen_string_literal: true

require 'rack/utils'

class Reynard
module Coders
# Generates a multi part form data request body.
class MultipartFormData
# We use all lowercase and no punctiation to be nice to other implementation.
ALPHABET = ('a'..'z').to_a + ('0'..'9').to_a
MULTIPART_BOUNDARY = 0.upto(69).map { ALPHABET.sample }.join.freeze

# Returns the multipart boundary generated for this request body.
attr_reader :multipart_boundary

def initialize(data:)
@data = data
end

def mime_type
'multipart/form-data'
end

def headers
{ 'Content-Type' => %(#{mime_type}; boundary="#{MULTIPART_BOUNDARY}") }
end

def body
body = StringIO.new
@data.each do |name, value|
body << "--#{MULTIPART_BOUNDARY}\r"
case value
when Tempfile, IO
write_io_part(body, name, value)
else
write_part(body, name, value)
end
end
body.string
end

def write_io_part(body, name, io)
filename = self.class.filename(io)
filename = Rack::Utils.escape_path(File.basename(filename)) if filename
content_type = io.respond_to?(:content_type) ? io.content_type : nil
content_length = File.stat(io).size
content_attributes = { 'name' => name, 'filename' => filename }

body << %(Content-Disposition: #{join('form-data', *serialize(content_attributes))}\r)
body << %(Content-Type: #{content_type}\r) if content_type
body << %(Content-Length: #{content_length}\r) if content_length
body << %(\r)
IO.copy_stream(io, body)
body << %(\r)
end

def write_part(body, name, value)
body << %(Content-Disposition: #{join('form-data', *serialize({ 'name' => name }))}\r)
body << %(\r)
body << value
body << %(\r)
end

def join(*items)
items.join('; ')
end

def serialize(attributes)
attributes.compact.flat_map do |name, value|
%(#{name}="#{quote(value)}")
end
end

def quote(name)
name.to_s.gsub('"', '\"')
end

def self.filename(io)
# A lot of IO related types are low level and don't necessarily respond to +respond_to?+, so
# we just yeet the method and hope it sticks.
%i[original_filename filename path].each do |method|
return io.public_send(method)
rescue NoMethodError
end
nil
end
end
end
end
24 changes: 24 additions & 0 deletions lib/reynard/coders/text_plain.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# frozen_string_literal: true

class Reynard
module Coders
# Generates a plain text request body.
class TextPlain
def initialize(data:)
@data = data
end

def mime_type
'text/plain'
end

def headers
{ 'Content-Type' => mime_type.to_s }
end

def body
@data.to_s
end
end
end
end
12 changes: 6 additions & 6 deletions lib/reynard/content.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ def pick_serializer(serializers)
return unless serializers

@keys.each do |key|
if serializers.key?(key)
return Serializer.new(
content_type: key,
serializer: serializers[key]
)
end
next unless serializers.key?(key)

return SerializerSelection.new(
content_type: key,
serializer_class: serializers[key]
)
end
nil
end
Expand Down
6 changes: 3 additions & 3 deletions lib/reynard/context.rb
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ def logger(logger)
def serializer(content_type, serializer)
copy(
request: {
serializers: @request_context.serializers.merge({ content_type => serializer })
serializers: @request_context.serializers.merge({ content_type => serializer }).compact
}
)
end
Expand Down Expand Up @@ -98,11 +98,11 @@ def copy(request: {}, response: {})
def build_request
Reynard::Http::Request.new(
request_context: @request_context,
serializer: pick_serializer
serializer_selection: serializer_selection
)
end

def pick_serializer
def serializer_selection
@specification
.content(@request_context.operation.node)
.pick_serializer(@request_context.serializers)
Expand Down
41 changes: 31 additions & 10 deletions lib/reynard/http/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@ class Http
class Request
attr_reader :uri

def initialize(request_context:, serializer:)
def initialize(request_context:, serializer_selection:)
@request_context = request_context
@serializer = serializer
@serializer_selection = serializer_selection
@uri = URI(@request_context.url)
end

Expand All @@ -28,22 +28,43 @@ def request_class

def request_headers
{
'User-Agent' => Reynard.user_agent,
'Content-Type' => @serializer&.content_type
}.compact.merge(@request_context.headers || {})
'User-Agent' => Reynard.user_agent
}
.merge(serializer&.headers || {})
.merge(@request_context.headers || {})
end

def build_request
request = request_class.new(uri, request_headers)
if @serializer
@request_context.logger&.debug { @request_context.body }
request.body = @serializer.dump(@request_context.body)
if serializer
write_serializer_body(request)
elsif @request_context.body
@request_context.logger&.debug { @request_context.body }
request.body = @request_context.body
write_serializer_params(request)
end
request
end

def write_serializer_body(request)
@request_context.logger&.debug { @request_context.body }
request.body = serializer.body
end

def write_serializer_params(request)
@request_context.logger&.debug { @request_context.body }
request.body = @request_context.body
end

def serializer
return @serializer if defined?(@serializer)

@serializer = build_serializer
end

def build_serializer
return nil unless @serializer_selection&.serializer_class

@serializer_selection.serializer_class.new(data: @request_context.body)
end
end
end
end
16 changes: 0 additions & 16 deletions lib/reynard/serializer.rb

This file was deleted.

11 changes: 11 additions & 0 deletions lib/reynard/serializer_selection.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# frozen_string_literal: true

class Reynard
# Wraps the choice for a serializer request content type, and returns headers and a serializer
# to generate a request body.
SerializerSelection = Struct.new(
:content_type,
:serializer_class,
keyword_init: true
)
end
12 changes: 10 additions & 2 deletions test/mocks/serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,21 @@

module Mocks
class Serializer
attr_reader :data
attr_reader :mime_type, :data

def initialize
@data = []
end

def dump(data)
def mime_type
'application/mocks'
end

def headers
{ 'Content-Type' => mime_type }
end

def write(data)
@data << data
end
end
Expand Down
Loading

0 comments on commit 8afa119

Please sign in to comment.