Skip to content

Commit

Permalink
Merge pull request #78 from graphql-devise/add-mount-method-option-sa…
Browse files Browse the repository at this point in the history
…nitizer

Add mount method option sanitizer
  • Loading branch information
mcelicalderon committed Apr 4, 2020
2 parents e7f3e4c + 0753bb2 commit fade264
Show file tree
Hide file tree
Showing 18 changed files with 449 additions and 75 deletions.
1 change: 1 addition & 0 deletions lib/graphql_devise.rb
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
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
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
@@ -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
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
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
@@ -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

0 comments on commit fade264

Please sign in to comment.