package GraphQL::Execution;
use 5.014;
use strict;
use warnings;
use Return::Type;
use Types::Standard -all;
use Types::TypeTiny -all;
use GraphQL::Type::Library -all;
use Function::Parameters;
use GraphQL::Parser;
use GraphQL::Error;
use JSON::MaybeXS;
use GraphQL::Debug qw(_debug);
use GraphQL::Introspection qw(
$SCHEMA_META_FIELD_DEF $TYPE_META_FIELD_DEF $TYPE_NAME_META_FIELD_DEF
);
=head1 NAME
GraphQL::Execution - Execute GraphQL queries
=cut
our $VERSION = '0.02';
my $JSON = JSON::MaybeXS->new->allow_nonref;
use constant DEBUG => $ENV{GRAPHQL_DEBUG}; # "DEBUG and" gets optimised out if false
=head1 SYNOPSIS
use GraphQL::Execution;
my $result = GraphQL::Execution->execute($schema, $doc, $root_value);
=head1 DESCRIPTION
Executes a GraphQL query, returns results.
=head1 METHODS
=head2 execute
my $result = GraphQL::Execution->execute(
$schema,
$doc,
$root_value,
$context_value,
$variable_values,
$operation_name,
$field_resolver,
);
=cut
method execute(
(InstanceOf['GraphQL::Schema']) $schema,
Str $doc,
Any $root_value = undef,
Any $context_value = undef,
Maybe[HashRef] $variable_values = undef,
Maybe[Str] $operation_name = undef,
Maybe[CodeLike] $field_resolver = undef,
) :ReturnType(HashRef) {
my $ast = GraphQL::Parser->parse($doc);
my $context = eval {
_build_context(
$schema,
$ast,
$root_value,
$context_value,
$variable_values,
$operation_name,
$field_resolver,
);
};
return { errors => [ { message => $@ } ] } if $@;
my $result = eval {
scalar _execute_operation(
$context,
$context->{operation},
$root_value,
);
};
if ($@) {
push @{ $context->{errors} }, GraphQL::Error->coerce($@); # TODO no mutate $context
}
my $wrapped = { data => $result };
if (@{ $context->{errors} }) {
return { errors => [ map { { message => $_->to_string } } @{$context->{errors}} ], %$wrapped };
} else {
return $wrapped;
}
}
fun _build_context(
(InstanceOf['GraphQL::Schema']) $schema,
ArrayRef[HashRef] $ast,
Any $root_value,
Any $context_value,
Maybe[HashRef] $variable_values,
Maybe[Str] $operation_name,
Maybe[CodeLike] $field_resolver,
) :ReturnType(HashRef) {
my %fragments = map {
($_->{name} => $_)
} map $_->{node}, grep $_->{kind} eq 'fragment', @$ast;
my @operations = grep $_->{kind} eq 'operation', @$ast;
die "No operations supplied." if !@operations;
die "Can only execute document containing fragments or operations"
if @$ast != keys(%fragments) + @operations;
my $operation = _get_operation($operation_name, \@operations);
{
schema => $schema,
fragments => \%fragments,
root_value => $root_value,
context_value => $context_value,
operation => $operation->{node},
variable_values => _variables_apply_defaults(
$schema,
$operation->{node}{variables} || {},
$variable_values || {},
),
field_resolver => $field_resolver || \&_default_field_resolver,
errors => [],
};
}
# takes each operation var: query q(a: String)
# applies to it supplied variable from web request
# if none, applies any defaults in the operation var: query q(a: String = "h")
# converts with graphql_to_perl (which also validates) to Perl values
# return { varname => { value => ..., type => $type } }
fun _variables_apply_defaults(
(InstanceOf['GraphQL::Schema']) $schema,
HashRef $operation_variables,
HashRef $variable_values,
) :ReturnType(HashRef) {
my @bad = grep {
! _lookup_type($schema, $operation_variables->{$_})->DOES('GraphQL::Role::Input');
} keys %$operation_variables;
die "Variable '\$$bad[0]' is type '@{[
_lookup_type($schema, $operation_variables->{$bad[0]})->to_string
]}' which cannot be used as an input type.\n" if @bad;
+{ map {
my $opvar = $operation_variables->{$_};
my $opvar_type = _lookup_type($schema, $opvar);
my $parsed_value;
my $maybe_value = $variable_values->{$_} // $opvar->{default_value};
eval { $parsed_value = $opvar_type->graphql_to_perl($maybe_value) };
die "Variable '\$$_' got invalid value @{[$JSON->canonical->encode($maybe_value)]}.\n$@"
if $@;
($_ => { value => $parsed_value, type => $opvar_type })
} keys %$operation_variables };
}
fun _lookup_type(
(InstanceOf['GraphQL::Schema']) $schema,
HashRef $typedef,
) :ReturnType(InstanceOf['GraphQL::Type']) {
my $type = $typedef->{type};
return $schema->name2type->{$type} // die "Unknown type '$type'.\n" if is_Str($type);
my ($wrapper_type, $wrapped) = @$type;
_lookup_type($schema, $wrapped)->$wrapper_type;
}
sub _get_operation {
my ($operation_name, $operations) = @_;
DEBUG and _debug('_get_operation', @_);
if (!$operation_name) {
die "Must provide operation name if query contains multiple operations."
if @$operations > 1;
return $operations->[0];
}
my @matching = grep $_->{node}{name} eq $operation_name, @$operations;
return $matching[0] if @matching == 1;
die "No operations matching '$operation_name' found.";
}
fun _execute_operation(
HashRef $context,
HashRef $operation,
Any $root_value,
) :ReturnType(HashRef) {
my $op_type = $operation->{operationType} || 'query';
my $type = $context->{schema}->$op_type;
my $fields = _collect_fields(
$context,
$type,
$operation->{selections},
{},
{},
);
my $path = [];
my $execute = $op_type eq 'mutation'
? \&_execute_fields_serially : \&_execute_fields;
my $result = eval {
$execute->($context, $type, $root_value, $path, $fields);
};
if ($@) {
push @{ $context->{errors} }, GraphQL::Error->coerce($@); # TODO no mutate $context
return {};
}
$result;
}
fun _collect_fields(
HashRef $context,
(InstanceOf['GraphQL::Type']) $runtime_type,
ArrayRef $selections,
Map[StrNameValid,ArrayRef[HashRef]] $fields_got,
Map[StrNameValid,Bool] $visited_fragments,
) :ReturnType(Map[StrNameValid,ArrayRef[HashRef]]) {
DEBUG and _debug('_collect_fields', $runtime_type->to_string, $fields_got, $selections);
for my $selection (@$selections) {
my $node = $selection->{node};
next if !_should_include_node($context, $node);
if ($selection->{kind} eq 'field') {
# TODO no mutate $fields_got
my $use_name = $node->{alias} || $node->{name};
push @{ $fields_got->{$use_name} }, $node;
} elsif ($selection->{kind} eq 'inline_fragment') {
next if !_fragment_condition_match($context, $node, $runtime_type);
next if !_should_include_node($context, $node);
_collect_fields(
$context,
$runtime_type,
$node->{selections},
$fields_got,
$visited_fragments,
);
} elsif ($selection->{kind} eq 'fragment_spread') {
my $frag_name = $node->{name};
next if $visited_fragments->{$frag_name};
next if !_should_include_node($context, $node);
$visited_fragments->{$frag_name} = 1;
my $fragment = $context->{fragments}{$frag_name};
next if !$fragment;
next if !_fragment_condition_match($context, $fragment, $runtime_type);
DEBUG and _debug('_collect_fields(fragment_spread)', $fragment);
_collect_fields(
$context,
$runtime_type,
$fragment->{selections},
$fields_got,
$visited_fragments,
);
}
}
$fields_got;
}
fun _should_include_node(
HashRef $context,
HashRef $node,
) :ReturnType(Bool) {
# TODO implement
1;
}
fun _fragment_condition_match(
HashRef $context,
HashRef $node,
(InstanceOf['GraphQL::Type']) $runtime_type,
) :ReturnType(Bool) {
DEBUG and _debug('_fragment_condition_match', $runtime_type->to_string, $node);
return 1 if !$node->{on};
return 1 if $node->{on} eq $runtime_type->name;
my $condition_type = $context->{schema}->name2type->{$node->{on}} //
die GraphQL::Error->new(
message => "Unknown type for fragment condition '$node->{on}'."
);
return '' if !$condition_type->DOES('GraphQL::Role::Abstract');
$context->{schema}->is_possible_type($condition_type, $runtime_type);
}
fun _execute_fields(
HashRef $context,
(InstanceOf['GraphQL::Type']) $parent_type,
Any $root_value,
ArrayRef $path,
Map[StrNameValid,ArrayRef[HashRef]] $fields,
) :ReturnType(Map[StrNameValid,Any]){
my %results;
DEBUG and _debug('_execute_fields', $parent_type->to_string, $fields, $root_value);
map {
my $result_name = $_;
my $result = _resolve_field(
$context,
$parent_type,
$root_value,
[ @$path, $result_name ],
$fields->{$_},
);
$results{$result_name} = $result;
# TODO promise stuff
} keys %$fields; # TODO ordering of fields
\%results;
}
fun _execute_fields_serially(
HashRef $context,
(InstanceOf['GraphQL::Type']) $parent_type,
Any $root_value,
ArrayRef $path,
Map[StrNameValid,ArrayRef[HashRef]] $fields,
) {
DEBUG and _debug('_execute_fields_serially', $parent_type->to_string, $fields, $root_value);
# TODO implement
goto &_execute_fields;
}
# NB same ordering as _execute_fields - graphql-js switches last 2
fun _resolve_field(
HashRef $context,
(InstanceOf['GraphQL::Type']) $parent_type,
Any $root_value,
ArrayRef $path,
ArrayRef[HashRef] $nodes,
) {
my $field_node = $nodes->[0];
my $field_name = $field_node->{name};
DEBUG and _debug('_resolve_field', $parent_type->to_string, $nodes, $root_value);
my $field_def = _get_field_def($context->{schema}, $parent_type, $field_name);
return if !$field_def;
my $resolve = $field_def->{resolve} || $context->{field_resolver};
my $info = _build_resolve_info(
$context,
$parent_type,
$field_def,
$path,
$nodes,
);
my $result = _resolve_field_value_or_error(
$context,
$field_def,
$nodes,
$resolve,
$root_value,
$info,
);
_complete_value_catching_error(
$context,
$field_def->{type},
$nodes,
$info,
$path,
$result,
);
}
use constant FIELDNAME2SPECIAL => {
map { ($_->{name} => $_) } $SCHEMA_META_FIELD_DEF, $TYPE_META_FIELD_DEF
};
fun _get_field_def(
(InstanceOf['GraphQL::Schema']) $schema,
(InstanceOf['GraphQL::Type']) $parent_type,
StrNameValid $field_name,
) :ReturnType(HashRef) {
return $TYPE_NAME_META_FIELD_DEF
if $field_name eq $TYPE_NAME_META_FIELD_DEF->{name};
return FIELDNAME2SPECIAL->{$field_name}
if FIELDNAME2SPECIAL->{$field_name} and $parent_type == $schema->query;
$parent_type->fields->{$field_name} //
die GraphQL::Error->new(
message => "No field @{[$parent_type->name]}.$field_name."
);
}
# NB similar ordering as _execute_fields - graphql-js switches
fun _build_resolve_info(
HashRef $context,
(InstanceOf['GraphQL::Type']) $parent_type,
HashRef $field_def,
ArrayRef $path,
ArrayRef[HashRef] $nodes,
) {
{
field_name => $nodes->[0]{name},
field_nodes => $nodes,
return_type => $field_def->{type},
parent_type => $parent_type,
path => $path,
schema => $context->{schema},
fragments => $context->{fragments},
root_value => $context->{root_value},
operation => $context->{operation},
variable_values => $context->{variable_values},
};
}
fun _resolve_field_value_or_error(
HashRef $context,
HashRef $field_def,
ArrayRef[HashRef] $nodes,
Maybe[CodeLike] $resolve,
Maybe[Any] $root_value,
HashRef $info,
) {
DEBUG and _debug('_resolve_field_value_or_error', $nodes, $root_value, $field_def, $JSON->encode($nodes->[0]));
my $result = eval {
my $args = _get_argument_values($field_def, $nodes->[0], $context->{variable_values});
DEBUG and _debug("_resolve_field_value_or_error(resolve)", $args, $JSON->encode($args));
$resolve->($root_value, $args, $context->{context_value}, $info);
};
return GraphQL::Error->coerce($@) if $@;
$result;
}
fun _complete_value_catching_error(
HashRef $context,
(InstanceOf['GraphQL::Type']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
ArrayRef $path,
Any $result,
) {
if ($return_type->isa('GraphQL::Type::NonNull')) {
return _complete_value_with_located_error(@_);
}
my $result = eval {
my $completed = _complete_value_with_located_error(@_);
# TODO promise stuff
$completed;
};
if ($@) {
push @{ $context->{errors} }, GraphQL::Error->coerce($@);
return undef; # null value
}
$result;
}
fun _complete_value_with_located_error(
HashRef $context,
(InstanceOf['GraphQL::Type']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
ArrayRef $path,
Any $result,
) {
my $result = eval {
my $completed = _complete_value(@_);
# TODO promise stuff
$completed;
};
if ($@) {
die _located_error($@, $nodes, $path);
}
$result;
}
fun _complete_value(
HashRef $context,
(InstanceOf['GraphQL::Type']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
ArrayRef $path,
Any $result,
) {
DEBUG and _debug('_complete_value', $return_type->to_string, $result);
# TODO promise stuff
die $result if GraphQL::Error->is($result);
if ($return_type->isa('GraphQL::Type::NonNull')) {
my $completed = _complete_value(
$context,
$return_type->of,
$nodes,
$info,
$path,
$result,
);
die GraphQL::Error->new(
message => "Cannot return null for non-nullable field @{[$info->{parent_type}->name]}.@{[$info->{field_name}]}."
) if !defined $completed;
return $completed;
}
return $result if !defined $result;
return _complete_list_value(@_) if $return_type->isa('GraphQL::Type::List');
return _complete_leaf_value($return_type, $result)
if $return_type->DOES('GraphQL::Role::Leaf');
return _complete_abstract_value(@_) if $return_type->DOES('GraphQL::Role::Abstract');
return _complete_object_value(@_) if $return_type->isa('GraphQL::Type::Object');
# shouldn't get here
die GraphQL::Error->new(
message => "Cannot complete value of unexpected type '@{[$return_type->to_string]}'."
);
}
fun _complete_list_value(
HashRef $context,
(InstanceOf['GraphQL::Type::List']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
ArrayRef $path,
ArrayRef $result,
) {
# TODO promise stuff
my $item_type = $return_type->of;
my $index = 0;
my @completed_results = map {
_complete_value_catching_error(
$context,
$item_type,
$nodes,
$info,
[ @$path, $index++ ],
$_,
);
} @$result;
\@completed_results;
}
fun _complete_leaf_value(
(ConsumerOf['GraphQL::Role::Leaf']) $return_type,
Any $result,
) {
DEBUG and _debug('_complete_leaf_value', $return_type->to_string, $result);
my $serialised = $return_type->perl_to_graphql($result);
die GraphQL::Error->new(message => "Expected a value of type '@{[$return_type->to_string]}' but received: '$result'.\n$@") if $@;
$serialised;
}
fun _complete_abstract_value(
HashRef $context,
(ConsumerOf['GraphQL::Role::Abstract']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
ArrayRef $path,
Any $result,
) {
my $runtime_type = ($return_type->resolve_type || \&_default_resolve_type)->(
$result, $context->{context_value}, $info, $return_type
);
# TODO promise stuff
_complete_object_value(
$context,
_ensure_valid_runtime_type(
$runtime_type,
$context,
$return_type,
$nodes,
$info,
$result,
),
$nodes,
$info,
$path,
$result,
);
}
fun _ensure_valid_runtime_type(
(Str | InstanceOf['GraphQL::Type::Object']) $runtime_type_or_name,
HashRef $context,
(ConsumerOf['GraphQL::Role::Abstract']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
Any $result,
) :ReturnType(InstanceOf['GraphQL::Type::Object']) {
my $runtime_type = is_InstanceOf($runtime_type_or_name)
? $runtime_type_or_name
: $context->{schema}->name2type->{$runtime_type_or_name};
die GraphQL::Error->new(
message => "Abstract type @{[$return_type->name]} must resolve to an " .
"Object type at runtime for field @{[$info->{parent_type}->name]}." .
"@{[$info->{field_name}]} with value $result, received '@{[$runtime_type->name]}'.",
nodes => [ $nodes ],
) if !$runtime_type->isa('GraphQL::Type::Object');
die GraphQL::Error->new(
message => "Runtime Object type '@{[$runtime_type->name]}' is not a possible type for " .
"'@{[$return_type->name]}'.",
nodes => [ $nodes ],
) if !$context->{schema}->is_possible_type($return_type, $runtime_type);
$runtime_type;
}
fun _default_resolve_type(
Any $value,
Any $context,
HashRef $info,
(ConsumerOf['GraphQL::Role::Abstract']) $abstract_type,
) {
my @possibles = @{ $info->{schema}->get_possible_types($abstract_type) };
# TODO promise stuff
(grep $_->is_type_of->($value, $context, $info), grep $_->is_type_of, @possibles)[0];
}
fun _complete_object_value(
HashRef $context,
(InstanceOf['GraphQL::Type::Object']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
ArrayRef $path,
Any $result,
) {
if ($return_type->is_type_of) {
my $is_type_of = $return_type->is_type_of->($result, $context->{context_value}, $info);
# TODO promise stuff
die GraphQL::Error->new(message => "Expected a value of type '@{[$return_type->to_string]}' but received: '$result'") if !$is_type_of;
}
_collect_and_execute_subfields(
$context,
$return_type,
$nodes,
$info,
$path,
$result,
);
}
fun _collect_and_execute_subfields(
HashRef $context,
(InstanceOf['GraphQL::Type::Object']) $return_type,
ArrayRef[HashRef] $nodes,
HashRef $info,
ArrayRef $path,
Any $result,
) {
my $subfield_nodes = {};
my $visited_fragment_names = {};
for (grep $_->{selections}, @$nodes) {
$subfield_nodes = _collect_fields(
$context,
$return_type,
$_->{selections},
$subfield_nodes,
$visited_fragment_names,
);
}
DEBUG and _debug('_collect_and_execute_subfields', $return_type->to_string, $subfield_nodes);
_execute_fields($context, $return_type, $result, $path, $subfield_nodes);
}
fun _located_error(
Any $error,
ArrayRef[HashRef] $nodes,
ArrayRef $path,
) {
# TODO implement
GraphQL::Error->coerce($error);
}
fun _get_argument_values(
HashRef $def,
HashRef $node,
Maybe[HashRef] $variable_values = {},
) {
my $arg_defs = $def->{args};
my $arg_nodes = $node->{arguments};
DEBUG and _debug("_get_argument_values", $arg_defs, $arg_nodes, $variable_values, $JSON->encode($node));
return {} if !$arg_defs;
my @bad = grep { !exists $arg_nodes->{$_} and !defined $arg_defs->{$_}{default_value} and $arg_defs->{$_}{type}->isa('GraphQL::Type::NonNull') } keys %$arg_defs;
die GraphQL::Error->new(
message => "Argument '$bad[0]' of type ".
"'@{[$arg_defs->{$bad[0]}{type}->to_string]}' not given.",
nodes => [ $node ],
) if @bad;
@bad = grep {
ref($arg_nodes->{$_}) eq 'SCALAR' and
$variable_values->{${$arg_nodes->{$_}}} and
!_type_will_accept($arg_defs->{$_}{type}, $variable_values->{${$arg_nodes->{$_}}}{type})
} keys %$arg_defs;
die GraphQL::Error->new(
message => "Variable '\$${$arg_nodes->{$bad[0]}}' of type '@{[$variable_values->{${$arg_nodes->{$bad[0]}}}{type}->to_string]}'".
" where expected '@{[$arg_defs->{$bad[0]}{type}->to_string]}'.",
nodes => [ $node ],
) if @bad;
my @novar = grep {
ref($arg_nodes->{$_}) eq 'SCALAR' and
(!$variable_values or !exists $variable_values->{${$arg_nodes->{$_}}}) and
!defined $arg_defs->{$_}{default_value} and
$arg_defs->{$_}{type}->isa('GraphQL::Type::NonNull')
} keys %$arg_defs;
die GraphQL::Error->new(
message => "Argument '$novar[0]' of type ".
"'@{[$arg_defs->{$novar[0]}{type}->to_string]}'".
" was given variable '\$${$arg_nodes->{$novar[0]}}' but no runtime value.",
nodes => [ $node ],
) if @novar;
my @enumfail = grep {
ref($arg_nodes->{$_}) eq 'REF' and
ref(${$arg_nodes->{$_}}) eq 'SCALAR' and
!$arg_defs->{$_}{type}->isa('GraphQL::Type::Enum')
} keys %$arg_defs;
die GraphQL::Error->new(
message => "Argument '$enumfail[0]' of type ".
"'@{[$arg_defs->{$enumfail[0]}{type}->to_string]}'".
" was given ${${$arg_nodes->{$enumfail[0]}}} which is enum value.",
nodes => [ $node ],
) if @enumfail;
my @enumstring = grep {
defined($arg_nodes->{$_}) and
!ref($arg_nodes->{$_})
} grep $arg_defs->{$_}{type}->isa('GraphQL::Type::Enum'), keys %$arg_defs;
die GraphQL::Error->new(
message => "Argument '$enumstring[0]' of type ".
"'@{[$arg_defs->{$enumstring[0]}{type}->to_string]}'".
" was given '$arg_nodes->{$enumstring[0]}' which is not enum value.",
nodes => [ $node ],
) if @enumstring;
return {} if !$arg_nodes;
my %coerced_values;
for my $name (keys %$arg_defs) {
my $arg_def = $arg_defs->{$name};
my $arg_type = $arg_def->{type};
my $argument_node = $arg_nodes->{$name};
my $default_value = $arg_def->{default_value};
DEBUG and _debug("_get_argument_values($name)", $arg_def, $arg_type, $argument_node, $default_value);
if (!exists $arg_nodes->{$name}) {
# none given - apply type arg's default if any. already validated perl
$coerced_values{$name} = $default_value if exists $arg_def->{default_value};
next;
} elsif (ref($argument_node) eq 'SCALAR') {
# scalar ref means it's a variable. already validated perl
$coerced_values{$name} =
($variable_values && $variable_values->{$$argument_node} && $variable_values->{$$argument_node}{value})
// $default_value;
next;
} elsif (ref($argument_node) eq 'REF') {
# double ref means it's an enum value. JSON land, needs convert/validate
$coerced_values{$name} = $$$argument_node;
} else {
# query literal. JSON land, needs convert/validate
$coerced_values{$name} = $argument_node;
}
next if !exists $coerced_values{$name};
DEBUG and _debug("_get_argument_values($name after initial)", $arg_def, $arg_type, $argument_node, $default_value, $JSON->encode(\%coerced_values));
eval { $coerced_values{$name} = $arg_type->graphql_to_perl($coerced_values{$name}) };
DEBUG and _debug("_get_argument_values($name after coerce)", $JSON->encode(\%coerced_values));
if ($@) {
die GraphQL::Error->new(
message => "Argument '$name' got invalid value"
. " @{[$JSON->encode($coerced_values{$name})]}.\nExpected '"
. $arg_type->to_string . "'.",
nodes => [ $node ],
);
}
}
\%coerced_values;
}
fun _type_will_accept(
(ConsumerOf['GraphQL::Role::Input']) $arg_type,
(ConsumerOf['GraphQL::Role::Input']) $var_type,
) {
return 1 if $arg_type == $var_type;
$arg_type = $arg_type->of if $arg_type->isa('GraphQL::Type::NonNull');
$var_type = $var_type->of if $var_type->isa('GraphQL::Type::NonNull');
return 1 if $arg_type == $var_type;
'';
}
# $root_value is either a hash with fieldnames as keys and either data
# or coderefs as values
# OR it's just a coderef itself
# OR it's an object which gets tried for fieldname as method
# any code gets called with obvious args
fun _default_field_resolver(
CodeLike | HashRef | InstanceOf $root_value,
HashRef $args,
Any $context,
HashRef $info,
) {
my $field_name = $info->{field_name};
my $property = is_HashRef($root_value)
? $root_value->{$field_name}
: $root_value;
DEBUG and _debug('_default_field_resolver', $root_value, $field_name, $args, $property);
if (eval { CodeLike->($property); 1 }) {
DEBUG and _debug('_default_field_resolver', 'codelike');
return $property->($args, $context, $info);
}
if (is_InstanceOf($root_value) and $root_value->can($field_name)) {
DEBUG and _debug('_default_field_resolver', 'method');
return $root_value->$field_name($args, $context, $info);
}
$property;
}
1;