Skip to content

Make the null value customizable [wip] #151

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
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
10 changes: 7 additions & 3 deletions src/graphql.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
-export([
parse/1,
elaborate/1,
type_check/1, type_check_params/3,
type_check/1, type_check_params/3, type_check_params/4,
validate/1,
execute/1, execute/2
]).
Expand Down Expand Up @@ -99,7 +99,12 @@ elaborate(AST) ->

-spec type_check_params(any(), any(), any()) -> param_context().
type_check_params(FunEnv, OpName, Vars) ->
graphql_type_check:x_params(FunEnv, OpName, Vars).
graphql_type_check:x_params(#{null_value => owl}, FunEnv, OpName, Vars).

-spec type_check_params(map(), any(), any(), any()) -> param_context().
type_check_params(Ctx, FunEnv, OpName, Vars) ->
graphql_type_check:x_params(Ctx, FunEnv, OpName, Vars).


-spec execute(ast()) -> #{ atom() => json() }.
execute(AST) ->
Expand All @@ -123,4 +128,3 @@ insert_schema_definition(Defn) ->
-spec validate_schema() -> ok | {error, any()}.
validate_schema() ->
graphql_schema_validate:x().

1 change: 0 additions & 1 deletion src/graphql_enum_coerce.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,3 @@ input(_, _) -> {error, not_valid_enum_input}.

output(_, {enum, X}) -> {ok, X};
output(_, Str) when is_binary(Str) -> {ok, Str}.

128 changes: 72 additions & 56 deletions src/graphql_execute.erl

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion src/graphql_scalar_binary_coerce.erl
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@
input(_, X) -> {ok, X}.

output(_,B) when is_binary(B) -> {ok, B};
output(_,_) -> {ok, null}.
output(_,_) -> {ok, owl}.
2 changes: 1 addition & 1 deletion src/graphql_scalar_bool_coerce.erl
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,4 @@ output(<<"Bool">>, true) -> {ok, true};
output(<<"Bool">>, <<"true">>) -> {ok, true};
output(<<"Bool">>, false) -> {ok, false};
output(<<"Bool">>, <<"false">>) -> {ok, false};
output(_,_) -> {ok, null}.
output(_,_) -> {ok, owl}.
2 changes: 1 addition & 1 deletion src/graphql_scalar_float_coerce.erl
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,4 @@ input(_, X) ->

output(<<"Float">>, F) when is_float(F) -> {ok, F};
output(<<"Float">>,I) when is_integer(I) -> {ok, float(I)};
output(_,_) -> {ok, null}.
output(_,_) -> {ok, owl}.
2 changes: 1 addition & 1 deletion src/graphql_scalar_integer_coerce.erl
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ input(_, X) ->
{ok, X}.

output(<<"Int">>, I) when is_integer(I) -> {ok, I};
output(_,_) -> {ok, null}.
output(_,_) -> {ok, owl}.
20 changes: 10 additions & 10 deletions src/graphql_schema_canonicalize.erl
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,10 @@ x({union, #{ id := ID, description := Desc, types := Types } = U}) ->
x({enum, #{ id := ID, description := Desc, values := VDefs} = Enum}) ->
ModuleResolver = enum_resolve(Enum),
#enum_type {
id = c_id(ID),
description = binarize(Desc),
annotations = annotations(Enum),
values = map_2(fun c_enum_val/2, VDefs),
id = c_id(ID),
description = binarize(Desc),
annotations = annotations(Enum),
values = map_2(fun c_enum_val/2, VDefs),
resolve_module = ModuleResolver
};

Expand Down Expand Up @@ -111,12 +111,12 @@ c_field(K, V) ->

c_field_val(M) ->
#schema_field {
ty = c_field_val_ty(M),
resolve = c_field_val_resolve(M),
args = c_field_val_args(M),
deprecation = deprecation(M),
annotations = annotations(M),
description = c_field_val_description(M)
ty = c_field_val_ty(M),
resolve = c_field_val_resolve(M),
args = c_field_val_args(M),
deprecation = deprecation(M),
annotations = annotations(M),
description = c_field_val_description(M)
}.

c_field_val_ty(#{ type := Ty }) ->
Expand Down
106 changes: 57 additions & 49 deletions src/graphql_type_check.erl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
-include("graphql_internal.hrl").
-include("graphql_schema.hrl").

-export([x/1, x_params/3]).
-export([x/1, x_params/3, x_params/4]).
-export([err_msg/1]).

%% -- TOP LEVEL TYPE CHECK CODE -------------------------------
Expand All @@ -57,8 +57,9 @@
x(Doc) ->
x(#{}, Doc).

x(Ctx, {document, Clauses}) ->
type_check(Ctx, [document], Clauses).
x(Ctx0, {document, Clauses}) ->
Ctx1 = Ctx0#{null_value => maps:get(null_value, Ctx0, owl)},
type_check(Ctx1, [document], Clauses).

type_check(Ctx, Path, Clauses) ->
{Fragments, _Rest} = lists:partition(
Expand Down Expand Up @@ -113,22 +114,26 @@ get_operation(FunEnv, OpName, _Params) ->
%% operation and its parameters matches the types in the operation referenced
-spec x_params(any(), any(), any()) -> graphql:param_context().
x_params(FunEnv, OpName, Params) ->
x_params(#{}, FunEnv, OpName, Params).

-spec x_params(any(), any(), any(), any()) -> graphql:param_context().
x_params(Ctx, FunEnv, OpName, Params) ->
case get_operation(FunEnv, OpName, Params) of
undefined ->
#{};
not_found ->
err([], {operation_not_found, OpName});
TyVarEnv ->
tc_params([OpName], TyVarEnv, Params)
tc_params(Ctx, [OpName], TyVarEnv, Params)
end.

%% Parameter checking has positive polarity, so we fold over
%% the type var environment from the schema and verify that each
%% type is valid
tc_params(Path, TyVarEnv, InitialParams) ->
tc_params(Ctx, Path, TyVarEnv, InitialParams) ->
F =
fun(K, V0, PS) ->
case tc_param(Path, K, V0, maps:get(K, PS, not_found)) of
case tc_param(Ctx, Path, K, V0, maps:get(K, PS, not_found)) of
V0 -> PS;
V1 -> PS#{ K => V1 }
end
Expand All @@ -139,31 +144,35 @@ tc_params(Path, TyVarEnv, InitialParams) ->
%% If a given parameter is not given, and there is a default, we can supply
%% the default value in some cases. The spec requires special handling of
%% null values, which are handled here.
tc_param(Path, K, #vardef { ty = {non_null, _}, default = null }, not_found) ->
tc_param(_Ctx, Path, K, #vardef { ty = {non_null, _}, default = null }, not_found) ->
err([K | Path], missing_non_null_param);
tc_param(Path, K, #vardef { default = Default,
tc_param(Ctx, Path, K, #vardef { default = Default,
ty = Ty }, not_found) ->
coerce_default_param([K | Path], Ty, Default);
tc_param(Path, K, #vardef { ty = Ty }, Val) ->
check_param([K | Path], Ty, Val).
coerce_default_param(Ctx, [K | Path], Ty, Default);
tc_param(#{null_value := _} = Ctx, Path, K, #vardef { ty = Ty }, Val) ->
check_param(Ctx, [K | Path], Ty, Val).

%% When checking params, the top level has been elaborated by the
%% elaborator, but the levels under it has not. So we have a case where
%% we need to look up underlying types and check them.
%%
%% This function case-splits on different types of positive polarity and
%% calls out to the correct helper-function
check_param(Path, {non_null, _}, null) -> err(Path, non_null);
check_param(Path, {non_null, Ty}, V) -> check_param(Path, Ty, V);
check_param(_Path, _Ty, null) -> null;
check_param(Path, {list, T}, L) when is_list(L) ->
%% check_param(Path, Ty, V) -> check_param(#{}, Path, Ty, V).

check_param(#{null_value := NV} = _Ctx, Path, {non_null, _}, NV) -> err(Path, non_null); % ?
check_param(_Ctx, Path, {non_null, _}, null) -> err(Path, non_null); % ?
check_param(Ctx, Path, {non_null, Ty}, V) -> check_param(Ctx, Path, Ty, V);
%% check_param(#{null_value := NV} = _Ctx, _Path, _Ty, NV) -> NV; % ? HERE!
check_param(#{null_value := NV} = _Ctx, _Path, _Ty, null) -> NV; % HERE!
check_param(Ctx, Path, {list, T}, L) when is_list(L) ->
%% Build a dummy structure to match the recursor. Unwrap this
%% structure before replacing the list parameter.
[check_param(Path, T, X) || X <- L];
check_param(Path, #scalar_type{} = STy, V) -> non_polar_coerce(Path, STy, V);
check_param(Path, #enum_type{} = ETy, {enum, V}) when is_binary(V) ->
check_param(Path, ETy, V);
check_param(Path, #enum_type { id = Ty }, V) when is_binary(V) ->
[check_param(Ctx, Path, T, X) || X <- L];
check_param(_Ctx, Path, #scalar_type{} = STy, V) -> non_polar_coerce(Path, STy, V);
check_param(Ctx, Path, #enum_type{} = ETy, {enum, V}) when is_binary(V) ->
check_param(Ctx, Path, ETy, V);
check_param(_Ctx, Path, #enum_type { id = Ty }, V) when is_binary(V) ->
%% Determine the type of any enum term, and then coerce it
case graphql_schema:lookup_enum_type(V) of
#enum_type { id = Ty } = ETy ->
Expand All @@ -173,30 +182,30 @@ check_param(Path, #enum_type { id = Ty }, V) when is_binary(V) ->
OtherTy ->
err(Path, {param_mismatch, {enum, Ty, OtherTy}})
end;
check_param(Path, #input_object_type{} = IOType, Obj) when is_map(Obj) ->
check_param(Ctx, Path, #input_object_type{} = IOType, Obj) when is_map(Obj) ->
%% When an object comes in through JSON for example, then the input object
%% will be a map which is already unique in its fields. To handle this, turn
%% the object into the same form as the one we use on query documents and pass
%% it on. Note that the code will create a map later on once the input has been
%% uniqueness-checked.
check_param(Path, IOType, {input_object, maps:to_list(Obj)});
check_param(Path, #input_object_type{} = IOType, {input_object, KVPairs}) ->
check_input_object(Path, IOType, {input_object, KVPairs});
check_param(Ctx, Path, IOType, {input_object, maps:to_list(Obj)});
check_param(Ctx, Path, #input_object_type{} = IOType, {input_object, KVPairs}) ->
check_input_object(Ctx, Path, IOType, {input_object, KVPairs});
%% The following expands un-elaborated (nested) types
check_param(Path, Ty, V) when is_binary(Ty) ->
check_param(Ctx, Path, Ty, V) when is_binary(Ty) ->
case graphql_schema:lookup(Ty) of
#scalar_type {} = ScalarTy -> non_polar_coerce(Path, ScalarTy, V);
#input_object_type {} = IOType -> check_input_object(Path, IOType, V);
#enum_type {} = Enum -> check_param(Path, Enum, V);
#input_object_type {} = IOType -> check_input_object(Ctx, Path, IOType, V);
#enum_type {} = Enum -> check_param(Ctx, Path, Enum, V);
_ ->
err(Path, {not_input_type, Ty, V})
end;
%% Everything else are errors
check_param(Path, Ty, V) ->
check_param(_Ctx, Path, Ty, V) ->
err(Path, {param_mismatch, Ty, V}).

coerce_default_param(Path, Ty, Default) ->
try check_param(Path, Ty, Default) of
coerce_default_param(Ctx, Path, Ty, Default) ->
try check_param(Ctx, Path, Ty, Default) of
Result -> Result
catch
Class:Err ->
Expand All @@ -209,22 +218,21 @@ coerce_default_param(Path, Ty, Default) ->
end.

%% Input objects are first coerced. Then they are checked.
check_input_object(Path, #input_object_type{ fields = Fields }, Obj) ->
Coerced = coerce_input_object(Path, Obj),
check_input_object_fields(Path, maps:to_list(Fields), Coerced, #{}).
check_input_object(Ctx, Path, #input_object_type{ fields = Fields }, Obj) ->
Coerced = coerce_input_object(Ctx, Path, Obj),
check_input_object_fields(Ctx, Path, maps:to_list(Fields), Coerced, #{}).

%% Input objects are in positive polarity, so the schema's fields are used
%% to verify that every field is present, and that there are no excess fields
%% As we process fields in the object, we remove them so we can check that
%% there are no more fields in the end.
check_input_object_fields(Path, [], Obj, Result) ->
check_input_object_fields(_Ctx, Path, [], Obj, Result) ->
case maps:size(Obj) of
0 -> Result;
K when K > 0 -> err(Path, {excess_fields_in_object, Obj})
end;
check_input_object_fields(Path,
[{Name, #schema_arg { ty = Ty,
default = Default }} | Next],
check_input_object_fields(#{null_value := _} = Ctx, Path,
[{Name, #schema_arg { ty = Ty, default = Default }} | Next],
Obj,
Result) ->
CoercedVal = case maps:get(Name, Obj, not_found) of
Expand All @@ -233,12 +241,12 @@ check_input_object_fields(Path,
{non_null, _} when Default == null ->
err([Name | Path], missing_non_null_param);
_ ->
coerce_default_param(Path, Ty, Default)
coerce_default_param(Ctx, Path, Ty, Default)
end;
V ->
check_param([Name | Path], Ty, V)
check_param(Ctx, [Name | Path], Ty, V)
end,
check_input_object_fields(Path,
check_input_object_fields(Ctx, Path,
Next,
maps:remove(Name, Obj),
Result#{ Name => CoercedVal }).
Expand Down Expand Up @@ -392,7 +400,7 @@ args(_Ctx, _Path, [], [], Acc) -> Acc;
args(_Ctx, Path, [_|_] = Args, [], _Acc) ->
err(Path, {excess_args, Args});
args(Ctx, Path, Args, [{Name, #schema_arg { ty = STy }} = SArg | Next], Acc) ->
case take_arg(Args, SArg) of
case take_arg(Ctx, Args, SArg) of
{error, Reason} ->
err([Name | Path], Reason);
{ok, {_, #{ type := Ty, value := Val}} = A, NextArgs} ->
Expand All @@ -413,14 +421,14 @@ args(Ctx, Path, Args, [{Name, #schema_arg { ty = STy }} = SArg | Next], Acc) ->
%% values correctly as we are conducting the search. Return both the
%% arg found and the remaining set of arguments so we can eventually
%% check if we exhausted the full set.
take_arg(Args, {Key, #schema_arg { ty = {non_null, _}, default = null}}) ->
take_arg(_Ctx, Args, {Key, #schema_arg { ty = {non_null, _}, default = null}}) ->
case lists:keytake(Key, 1, Args) of
false ->
{error, missing_non_null_param};
{value, Arg, NextArgs} ->
{ok, Arg, NextArgs}
end;
take_arg(Args, {Key, #schema_arg { ty = Ty, default = Default }}) ->
take_arg(_Ctx, Args, {Key, #schema_arg { ty = Ty, default = Default }}) ->
case lists:keytake(Key, 1, Args) of
false ->
{ok, {Key, #{ type => Ty, value => Default }}, Args};
Expand Down Expand Up @@ -622,11 +630,11 @@ judge(_Ctx, Path, {enum, N}, SType) ->
#{ document => Other,
schema => SType }})
end;
judge(_Ctx, Path, {input_object, _} = InputObj, SType) ->
judge(Ctx, Path, {input_object, _} = InputObj, SType) ->
case SType of
#input_object_type{} = IOType ->
Coerced = coerce_input_object(Path, InputObj),
check_input_object(Path, IOType, Coerced);
Coerced = coerce_input_object(Ctx, Path, InputObj),
check_input_object(Ctx, Path, IOType, Coerced);
_OtherType ->
err(Path, {type_mismatch, #{ document => InputObj, schema => SType }})
end;
Expand All @@ -646,18 +654,18 @@ judge(_Ctx, Path, Value, Unknown) ->
coerce_name(B) when is_binary(B) -> B;
coerce_name(Name) -> graphql_ast:name(Name).

coerce_input_object(Path, {input_object, Elems}) ->
coerce_input_object(Ctx, Path, {input_object, Elems}) ->
AssocList = [begin
N = coerce_name(K),
{N, coerce_input_object([N | Path], V)}
{N, coerce_input_object(Ctx, [N | Path], V)}
end || {K, V} <- Elems],
case graphql_ast:uniq(AssocList) of
ok ->
maps:from_list(AssocList);
{not_unique, Key} ->
err(Path, {input_object_not_unique, Key})
end;
coerce_input_object(_Path, Value) -> Value.
coerce_input_object(_Ctx, _Path, Value) -> Value.

%% -- Error handling -------------------------------------

Expand Down
Loading