Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions lib/graphql_devise.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ class InvalidMountOptionsError < GraphqlDevise::Error; end
require 'graphql_devise/user_error'
require 'graphql_devise/detailed_user_error'

require 'graphql_devise/mount_method/option_sanitizer'
require 'graphql_devise/mount_method/options_validator'
require 'graphql_devise/mount_method/queries_preparer'
require 'graphql_devise/mount_method/mutations_preparer'
Expand Down
18 changes: 18 additions & 0 deletions lib/graphql_devise/mount_method/option_sanitizer.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require_relative 'supported_options'

module GraphqlDevise
module MountMethod
class OptionSanitizer
def initialize(options = {}, supported_options = MountMethod::SUPPORTED_OPTIONS)
@options = options
@supported_options = supported_options
end

def call!
@supported_options.each_with_object(Struct.new(*@supported_options.keys).new) do |(key, checker), result|
result[key] = checker.call!(@options[key], key)
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/graphql_devise/mount_method/option_sanitizers/array_checker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module GraphqlDevise
module MountMethod
module OptionSanitizers
class ArrayChecker
def initialize(element_type)
@element_type = element_type
@default_value = []
end

def call!(value, key)
return @default_value if value.blank?

unless value.instance_of?(Array)
raise GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. Array expected."
end

unless value.all? { |element| element.instance_of?(@element_type) }
raise GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has invalid elements. #{@element_type} expected."
end

value
end
end
end
end
end
26 changes: 26 additions & 0 deletions lib/graphql_devise/mount_method/option_sanitizers/class_checker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
module GraphqlDevise
module MountMethod
module OptionSanitizers
class ClassChecker
def initialize(klass)
@klass_array = Array(klass)
end

def call!(value, key)
return if value.nil?

unless value.instance_of?(Class)
raise GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. Class expected."
end

unless @klass_array.any? { |klass| value.ancestors.include?(klass) }
raise GraphqlDevise::InvalidMountOptionsError,
"`#{key}` option has an invalid value. #{@klass_array.join(', ')} or descendants expected. Got #{value}."
end

value
end
end
end
end
end
24 changes: 24 additions & 0 deletions lib/graphql_devise/mount_method/option_sanitizers/hash_checker.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
module GraphqlDevise
module MountMethod
module OptionSanitizers
class HashChecker
def initialize(element_type_array)
@element_type_array = Array(element_type_array)
@default_value = {}
end

def call!(value, key)
return @default_value if value.blank?

unless value.instance_of?(Hash)
raise GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. Hash expected. Got #{value.class}."
end

value.each { |internal_key, klass| ClassChecker.new(@element_type_array).call!(klass, "#{key} -> #{internal_key}") }

value
end
end
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
module GraphqlDevise
module MountMethod
module OptionSanitizers
class StringChecker
def initialize(default_string = nil)
@default_string = default_string
end

def call!(value, key)
return @default_string if value.blank?

unless value.instance_of?(String)
raise GraphqlDevise::InvalidMountOptionsError, "`#{key}` option has an invalid value. String expected."
end

value
end
end
end
end
end
Original file line number Diff line number Diff line change
Expand Up @@ -4,40 +4,20 @@ module GraphqlDevise
module MountMethod
module OptionValidators
class ProvidedOperationsValidator
def initialize(options: {}, supported_operations: {})
@options = options || {}
def initialize(options:, supported_operations:)
@options = options
@supported_operations = supported_operations
end

def validate!
skipped = @options.fetch(:skip, [])
only = @options.fetch(:only, [])
operations = @options.fetch(:operations, {})
supported_keys = @supported_operations.keys

raise_on_invalid_option_type!(:skip, skipped, Array)
raise_on_invalid_option_type!(:only, only, Array)
raise_on_invalid_option_type!(:operations, operations, Hash)

custom = operations.keys

[
SupportedOperationsValidator.new(provided_operations: skipped, key: :skip, supported_operations: supported_keys),
SupportedOperationsValidator.new(provided_operations: only, key: :only, supported_operations: supported_keys),
SupportedOperationsValidator.new(provided_operations: custom, key: :operations, supported_operations: supported_keys)
SupportedOperationsValidator.new(provided_operations: @options.skip, key: :skip, supported_operations: supported_keys),
SupportedOperationsValidator.new(provided_operations: @options.only, key: :only, supported_operations: supported_keys),
SupportedOperationsValidator.new(provided_operations: @options.operations.keys, key: :operations, supported_operations: supported_keys)
].each(&:validate!)
end

private

def raise_on_invalid_option_type!(key, value, expected_class)
unless value.is_a?(expected_class)
raise(
GraphqlDevise::InvalidMountOptionsError,
"#{key} option contains value of invalid value. Value must be #{expected_class.name}."
)
end
end
end
end
end
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@ module GraphqlDevise
module MountMethod
module OptionValidators
class SkipOnlyValidator
def initialize(options: {})
@options = options || {}
def initialize(options:)
@options = options
end

def validate!
if [@options[:skip], @options[:only]].all?(&:present?)
if [@options.skip, @options.only].all?(&:present?)
raise(
GraphqlDevise::InvalidMountOptionsError,
"Can't specify both `skip` and `only` options when mounting the route."
Expand Down
18 changes: 18 additions & 0 deletions lib/graphql_devise/mount_method/supported_options.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
require_relative 'option_sanitizers/array_checker'
require_relative 'option_sanitizers/hash_checker'
require_relative 'option_sanitizers/string_checker'
require_relative 'option_sanitizers/class_checker'

module GraphqlDevise
module MountMethod
SUPPORTED_OPTIONS = {
at: OptionSanitizers::StringChecker.new('/graphql_auth'),
operations: OptionSanitizers::HashChecker.new([GraphQL::Schema::Resolver, GraphQL::Schema::Mutation]),
only: OptionSanitizers::ArrayChecker.new(Symbol),
skip: OptionSanitizers::ArrayChecker.new(Symbol),
additional_queries: OptionSanitizers::HashChecker.new(GraphQL::Schema::Resolver),
additional_mutations: OptionSanitizers::HashChecker.new(GraphQL::Schema::Mutation),
authenticatable_type: OptionSanitizers::ClassChecker.new(GraphQL::Schema::Member)
}.freeze
end
end
36 changes: 16 additions & 20 deletions lib/graphql_devise/rails/routes.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,31 +12,25 @@ class Mapper

def mount_graphql_devise_for(resource, options = {})
default_operations = GraphqlDevise::DefaultOperations::MUTATIONS.merge(GraphqlDevise::DefaultOperations::QUERIES)
clean_options = GraphqlDevise::MountMethod::OptionSanitizer.new(options).call!

GraphqlDevise::MountMethod::OptionsValidator.new(
[
GraphqlDevise::MountMethod::OptionValidators::SkipOnlyValidator.new(options: options),
GraphqlDevise::MountMethod::OptionValidators::SkipOnlyValidator.new(options: clean_options),
GraphqlDevise::MountMethod::OptionValidators::ProvidedOperationsValidator.new(
options: options, supported_operations: default_operations
options: clean_options, supported_operations: default_operations
)
]
).validate!

custom_operations = options.fetch(:operations, {})
skipped_operations = options.fetch(:skip, [])
only_operations = options.fetch(:only, [])
additional_mutations = options.fetch(:additional_mutations, {})
additional_queries = options.fetch(:additional_queries, {})
path = options.fetch(:at, '/graphql_auth')
mapping_name = resource.underscore.tr('/', '_').to_sym
authenticatable_type = options[:authenticatable_type].presence ||
authenticatable_type = clean_options.authenticatable_type.presence ||
"Types::#{resource}Type".safe_constantize ||
GraphqlDevise::Types::AuthenticatableType

param_operations = {
custom: custom_operations,
only: only_operations,
skipped: skipped_operations
custom: clean_options.operations,
only: clean_options.only,
skipped: clean_options.skip
}

devise_for(
Expand All @@ -62,28 +56,30 @@ def mount_graphql_devise_for(resource, options = {})
authenticatable_type: authenticatable_type
)

prepared_mutations.merge(additional_mutations).each do |action, mutation|
all_mutations = prepared_mutations.merge(clean_options.additional_mutations)
all_mutations.each do |action, mutation|
GraphqlDevise::Types::MutationType.field(action, mutation: mutation)
end

if (prepared_mutations.present? || additional_mutations.present?) &&
if all_mutations.present? &&
(Gem::Version.new(GraphQL::VERSION) < Gem::Version.new('1.10.0') || GraphqlDevise::Schema.mutation.nil?)
GraphqlDevise::Schema.mutation(GraphqlDevise::Types::MutationType)
end

prepared_queries.merge(additional_queries).each do |action, resolver|
all_queries = prepared_queries.merge(clean_options.additional_queries)
all_queries.each do |action, resolver|
GraphqlDevise::Types::QueryType.field(action, resolver: resolver)
end

if (prepared_queries.blank? || additional_queries.present?) && GraphqlDevise::Types::QueryType.fields.blank?
if all_queries.present? && GraphqlDevise::Types::QueryType.fields.blank?
GraphqlDevise::Types::QueryType.field(:dummy, resolver: GraphqlDevise::Resolvers::Dummy)
end

Devise.mailer.helper(GraphqlDevise::MailerHelper)

devise_scope mapping_name do
post path, to: 'graphql_devise/graphql#auth'
get path, to: 'graphql_devise/graphql#auth'
devise_scope resource.underscore.tr('/', '_').to_sym do
post clean_options.at, to: 'graphql_devise/graphql#auth'
get clean_options.at, to: 'graphql_devise/graphql#auth'
end
end
end
Expand Down
4 changes: 2 additions & 2 deletions spec/services/mount_method/mutations_preparer_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,13 @@
let(:class1) { Class.new(GraphQL::Schema::Mutation) }
let(:class2) { GraphQL::Schema::Mutation }
let(:auth_type) { GraphqlDevise::Types::AuthenticatableType }
let(:mutations) { { mutation_1: class1, mutation_2: class2 } }
let(:mutations) { { mutation1: class1, mutation2: class2 } }

context 'when mutations is *NOT* empty' do
it 'assign gql attibutes to mutations and changes keys using resource map' do
result = subject

expect(result.keys).to contain_exactly(:user_mutation_1, :user_mutation_2)
expect(result.keys).to contain_exactly(:user_mutation1, :user_mutation2)
expect(result.values.map(&:graphql_name)).to contain_exactly(
'UserMutation1', 'UserMutation2'
)
Expand Down
85 changes: 85 additions & 0 deletions spec/services/mount_method/option_sanitizer_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
require 'spec_helper'

RSpec.describe GraphqlDevise::MountMethod::OptionSanitizer do
subject(:clean_options) { described_class.new(options, supported_options).call! }

describe '#call!' do
let(:supported_options) do
{
my_string: GraphqlDevise::MountMethod::OptionSanitizers::StringChecker.new('default string'),
hash_multiple: GraphqlDevise::MountMethod::OptionSanitizers::HashChecker.new([String, Numeric]),
array: GraphqlDevise::MountMethod::OptionSanitizers::ArrayChecker.new(Symbol),
hash_single: GraphqlDevise::MountMethod::OptionSanitizers::HashChecker.new(Float),
my_class: GraphqlDevise::MountMethod::OptionSanitizers::ClassChecker.new(Numeric)
}
end

context 'when all options are provided and correct' do
let(:options) do
{
my_string: 'non default',
hash_multiple: { first: String, second: Float, third: Float },
array: [:one, :two, :three],
hash_single: { first: Float, second: Float },
my_class: Float
}
end

it 'returns a struct with clean options' do
expect(
my_string: clean_options.my_string,
hash_multiple: clean_options.hash_multiple,
array: clean_options.array,
hash_single: clean_options.hash_single,
my_class: clean_options.my_class
).to match(
my_string: 'non default',
hash_multiple: { first: String, second: Float, third: Float },
array: [:one, :two, :three],
hash_single: { first: Float, second: Float },
my_class: Float
)
end
end

context 'when some options are provided but all correct' do
let(:options) do
{
hash_multiple: { first: String, second: Float, third: Float },
array: [:one, :two, :three],
my_class: Float
}
end

it 'returns a struct with clean options and default values' do
expect(
my_string: clean_options.my_string,
hash_multiple: clean_options.hash_multiple,
array: clean_options.array,
hash_single: clean_options.hash_single,
my_class: clean_options.my_class
).to match(
my_string: 'default string',
hash_multiple: { first: String, second: Float, third: Float },
array: [:one, :two, :three],
hash_single: {},
my_class: Float
)
end
end

context 'when an option provided is invalid' do
let(:options) do
{
hash_multiple: { first: String, second: Float, third: Float },
array: ['not symbol 1', 'not symbol 2'],
my_class: Float
}
end

it 'raises an error' do
expect { clean_options }.to raise_error(GraphqlDevise::InvalidMountOptionsError, '`array` option has invalid elements. Symbol expected.')
end
end
end
end
Loading