Skip to content

Commit

Permalink
Merge pull request #20 from samesystem/allow_to_send_files
Browse files Browse the repository at this point in the history
Allow sending files using multipart requests
  • Loading branch information
jusmat authored Jan 27, 2021
2 parents ac416c1 + eb5aeb8 commit 191ce14
Show file tree
Hide file tree
Showing 17 changed files with 531 additions and 28 deletions.
6 changes: 5 additions & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ PATH
activesupport (>= 4.0.0)
graphlient (>= 0.3)
graphql (>= 1.9.0)
mime-types (>= 3.0)

GEM
remote: https://rubygems.org/
Expand Down Expand Up @@ -55,6 +56,9 @@ GEM
jaro_winkler (1.5.4)
json (2.3.1)
method_source (1.0.0)
mime-types (3.3.1)
mime-types-data (~> 3.2015)
mime-types-data (3.2020.1104)
minitest (5.14.2)
multipart-post (2.1.1)
parallel (1.20.1)
Expand Down Expand Up @@ -129,4 +133,4 @@ DEPENDENCIES
webmock (~> 3)

BUNDLED WITH
2.0.1
2.2.6
1 change: 1 addition & 0 deletions active_graphql.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'graphlient', '>= 0.3'
spec.add_dependency 'activesupport', '>= 4.0.0'
spec.add_dependency 'activemodel', '>= 3.0.0'
spec.add_dependency 'mime-types', '>= 3.0'

spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "webmock", "~> 3"
Expand Down
20 changes: 16 additions & 4 deletions lib/active_graphql/client/actions/action.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
# frozen_string_literal: true

require 'active_graphql/client/actions/variable_detectable'
require 'active_graphql/client/actions/action/format_outputs'
require 'active_graphql/client/actions/action/format_inputs'
require 'active_graphql/client/actions/action/format_variable_inputs'

module ActiveGraphql
class Client
module Actions
# Base class for query/mutation action objects
class Action
class InvalidActionError < StandardError; end
include VariableDetectable

require 'active_graphql/client/actions/action/format_outputs'
require 'active_graphql/client/actions/action/format_inputs'
class InvalidActionError < StandardError; end

attr_reader :name, :type, :output_values, :client, :input_attributes, :meta_attributes

Expand Down Expand Up @@ -63,14 +67,18 @@ def to_graphql
assert_format

<<~TXT
#{type} {
#{type}#{wrapped_header formatted_variable_headers} {
#{name}#{wrapped_header formatted_inputs} {
#{formatted_outputs}
}
}
TXT
end

def graphql_variables
variable_attributes(input_attributes)
end

private

def join_array_and_hash(*array, **hash)
Expand All @@ -85,6 +93,10 @@ def formatted_outputs
FormatOutputs.new(output_values).call
end

def formatted_variable_headers
FormatVariableInputs.new(graphql_variables).call
end

def assert_format
return unless output_values.empty?

Expand Down
32 changes: 20 additions & 12 deletions lib/active_graphql/client/actions/action/format_inputs.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ module Actions
class Action
# converts ruby object in to grapqhl input string
class FormatInputs
include VariableDetectable

def initialize(inputs)
@inputs = inputs
end
Expand All @@ -20,9 +22,9 @@ def call

attr_reader :inputs

def formatted(attributes)
def formatted(attributes, parent_keys: [])
if attributes.is_a?(Hash)
formatted_attributes(attributes)
formatted_attributes(attributes, parent_keys: parent_keys)
else
raise(
ActiveGraphql::Client::Actions::WrongTypeError,
Expand All @@ -31,39 +33,45 @@ def formatted(attributes)
end
end

def formatted_attributes(attributes)
def formatted_attributes(attributes, parent_keys: [])
attributes = attributes.dup
keyword_fields = (attributes.delete(:__keyword_attributes) || []).map(&:to_s)

formatted_attributes = attributes.map do |key, val|
if keyword_fields.include?(key.to_s)
formatted_key_and_keyword(key, val)
formatted_key_and_keyword(key, val, parent_keys: parent_keys)
else
formatted_key_and_value(key, val)
formatted_key_and_value(key, val, parent_keys: parent_keys)
end
end

formatted_attributes.join(', ')
end

def formatted_key_and_value(key, value)
"#{key}: #{formatted_value(value)}"
def formatted_key_and_value(key, value, parent_keys:)
if variable_value?(value)
"#{key}: $#{[*parent_keys, key].compact.join('_')}"
else
"#{key}: #{formatted_value(value, parent_keys: [*parent_keys, key])}"
end
end

def formatted_key_and_keyword(key, value)
def formatted_key_and_keyword(key, value, parent_keys:)
if value.is_a?(String) || value.is_a?(Symbol)
"#{key}: #{value}"
else
"#{key}: #{formatted_value(value)}"
"#{key}: #{formatted_value(value, parent_keys: [*parent_keys, key])}"
end
end

def formatted_value(value) # rubocop:disable Metrics/MethodLength
def formatted_value(value, parent_keys:) # rubocop:disable Metrics/MethodLength
case value
when Hash
"{ #{formatted(value)} }"
"{ #{formatted(value, parent_keys: parent_keys)} }"
when Array
formatted_values = value.map { |it| formatted_value(it) }
formatted_values = value.map.with_index do |it, idx|
formatted_value(it, parent_keys: [*parent_keys, idx])
end
"[#{formatted_values.join(', ')}]"
when nil
'null'
Expand Down
53 changes: 53 additions & 0 deletions lib/active_graphql/client/actions/action/format_variable_inputs.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# frozen_string_literal: true

module ActiveGraphql
class Client
module Actions
class Action
# converts ruby object in to varbiable stype grapqhl input
class FormatVariableInputs
include VariableDetectable

def initialize(inputs)
@initial_inputs = inputs
end

def call
return '' if inputs.empty?

formatted_attributes(inputs)
end

private

attr_reader :initial_inputs

def formatted_attributes(attributes)
attributes = attributes.dup
formatted_attributes = attributes.map do |key, val|
formatted_key_and_type(key, val)
end

formatted_attributes.join(', ')
end

def inputs
@inputs ||= variable_attributes(initial_inputs)
end

def formatted_key_and_type(key, value)
"$#{key}: #{formatted_type(value)}"
end

def formatted_type(value)
if value.is_a?(Array)
'[File!]!'
else
'File!'
end
end
end
end
end
end
end
47 changes: 47 additions & 0 deletions lib/active_graphql/client/actions/variable_detectable.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# frozen_string_literal: true

module ActiveGraphql
class Client
module Actions
# handles all action details which are specific for query type request
module VariableDetectable
def variable_attributes(attributes)
variables_or_nil = attributes.transform_values do |value|
if value.is_a?(Hash)
variable_attributes(value)
elsif variable_value?(value)
value
elsif value.is_a?(Array)
variable_attributes(value.map.with_index { |val, i| [i, val] }.to_h)
end
end

flatten_keys(variables_or_nil).select { |_, val| val.present? }
end

def variable_value?(value)
kind_of_file?(value) || (value.is_a?(Array) && kind_of_file?(value.first))
end

private

def flatten_keys(attributes, parent_key: nil)
flattened = {}
attributes.each do |key, value|
full_key = [parent_key, key].compact.join('_').to_sym
if value.is_a?(Hash)
flattened.merge!(flatten_keys(value, parent_key: full_key))
else
flattened[full_key] = value
end
end
flattened
end

def kind_of_file?(value)
value.is_a?(File)
end
end
end
end
end
61 changes: 61 additions & 0 deletions lib/active_graphql/client/adapters/format_multipart_variables.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# frozen_string_literal: true

require 'faraday'
require 'mime/types'
require 'active_graphql/errors'

module ActiveGraphql
class Client
module Adapters
class NoMimeTypeException < ActiveGraphql::Errors::Error; end

# Converts deeply nested File instances to Faraday::UploadIO
class FormatMultipartVariables
def initialize(variables)
@variables = variables
end

def call
deep_transform_values(variables) do |variable|
variable_value(variable)
end
end

private

attr_reader :variables

def deep_transform_values(hash, &block)
return hash unless hash.is_a?(Hash)

hash.transform_values do |val|
if val.is_a?(Hash)
deep_transform_values(val, &block)
else
yield(val)
end
end
end

def variable_value(variable)
if variable.is_a?(Array)
variable.map { |it| variable_value(it) }
elsif variable.is_a?(Hash)
variable.transform_values { |it| variable_value(it) }
elsif variable.is_a?(File)
file_variable_value(variable)
else
variable
end
end

def file_variable_value(file)
content_type = MIME::Types.type_for(file.path).first
return Faraday::UploadIO.new(file.path, content_type) if content_type

raise NoMimeTypeException, "Unable to determine mime type for #{file.path}"
end
end
end
end
end
17 changes: 14 additions & 3 deletions lib/active_graphql/client/adapters/graphlient_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,37 @@ module Adapters
# Client which makes raw API requests to GraphQL server
class GraphlientAdapter
require 'graphlient'
require_relative './graphlient_multipart_adapter'

def initialize(config)
@url = config[:url]
@adapter_config = config.except(:url)
@config = config
end

def post(action)
raw_response = graphql_client.query(action.to_graphql)
raw_response = graphql_client.query(action.to_graphql, action.graphql_variables)
Response.new(raw_response.data)
rescue Graphlient::Errors::GraphQLError => e
Response.new(nil, e)
end

def adapter_config
@adapter_config ||= config.except(:url, :multipart).tap do |new_config|
new_config[:http] = GraphlientMultipartAdapter if multipart?
end
end

private

attr_reader :url, :adapter_config
attr_reader :url, :config

def graphql_client
@graphql_client ||= Graphlient::Client.new(url, **adapter_config)
end

def multipart?
config[:multipart].present?
end
end
end
end
Expand Down
Loading

0 comments on commit 191ce14

Please sign in to comment.