diff --git a/birdie_snapshots/mutation_function_generation.accepted b/birdie_snapshots/mutation_function_generation.accepted index c4b5641..076f400 100644 --- a/birdie_snapshots/mutation_function_generation.accepted +++ b/birdie_snapshots/mutation_function_generation.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Mutation function generation +file: ./test/codegen_test.gleam +test_name: generate_mutation_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/mutation_with_input_object_variable.accepted b/birdie_snapshots/mutation_with_input_object_variable.accepted new file mode 100644 index 0000000..edbaf42 --- /dev/null +++ b/birdie_snapshots/mutation_with_input_object_variable.accepted @@ -0,0 +1,82 @@ +--- +version: 1.4.1 +title: Mutation with InputObject variable +--- +import gleam/dynamic/decode +import gleam/http +import gleam/http/request +import gleam/httpc +import gleam/json +import gleam/list +import gleam/option.{type Option} +import gleam/result +import squall + +pub type ProfileInput { + ProfileInput(display_name: Option(String), description: Option(String)) +} + +fn profile_input_to_json(input: ProfileInput) -> json.Json { + json.object( + [ + #("displayName", json.nullable(input.display_name, json.string)), + #("description", json.nullable(input.description, json.string)), + ], + ) +} + +pub type Profile { + Profile(id: String, display_name: Option(String)) +} + +pub fn profile_decoder() -> decode.Decoder(Profile) { + use id <- decode.field("id", decode.string) + use display_name <- decode.field("displayName", decode.optional(decode.string)) + decode.success(Profile(id: id, display_name: display_name)) +} + +pub type UpdateProfileResponse { + UpdateProfileResponse(update_profile: Option(Profile)) +} + +pub fn update_profile_response_decoder() -> decode.Decoder(UpdateProfileResponse) { + use update_profile <- decode.field("updateProfile", decode.optional(profile_decoder())) + decode.success(UpdateProfileResponse(update_profile: update_profile)) +} + +pub fn update_profile(client: squall.Client, input: ProfileInput) -> Result(UpdateProfileResponse, String) { + let query = + "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }" + let variables = + json.object([#("input", profile_input_to_json(input))]) + let body = + json.object([#("query", json.string(query)), #("variables", variables)]) + use req <- result.try( + request.to(client.endpoint) + |> result.map_error(fn(_) { "Invalid endpoint URL" }), + ) + let req = + req + |> request.set_method(http.Post) + |> request.set_body(json.to_string(body)) + |> request.set_header("content-type", "application/json") + let req = + list.fold(client.headers, req, fn(r, header) { + request.set_header(r, header.0, header.1) + }) + use resp <- result.try( + httpc.send(req) + |> result.map_error(fn(_) { "HTTP request failed" }), + ) + use json_value <- result.try( + json.parse(from: resp.body, using: decode.dynamic) + |> result.map_error(fn(_) { "Failed to decode JSON response" }), + ) + let data_and_response_decoder = { + use data <- decode.field("data", update_profile_response_decoder()) + decode.success(data) + } + decode.run(json_value, data_and_response_decoder) + |> result.map_error(fn(_) { "Failed to decode response data" }) +} + diff --git a/birdie_snapshots/mutation_with_nested_input_object_types.accepted b/birdie_snapshots/mutation_with_nested_input_object_types.accepted new file mode 100644 index 0000000..b54d47b --- /dev/null +++ b/birdie_snapshots/mutation_with_nested_input_object_types.accepted @@ -0,0 +1,103 @@ +--- +version: 1.4.1 +title: Mutation with nested InputObject types +--- +import gleam/dynamic/decode +import gleam/http +import gleam/http/request +import gleam/httpc +import gleam/json +import gleam/list +import gleam/option.{type Option} +import gleam/result +import squall + +pub type BlobInput { + BlobInput(data: String, mime_type: String) +} + +fn blob_input_to_json(input: BlobInput) -> json.Json { + json.object( + [ + #("data", json.string(input.data)), + #("mimeType", json.string(input.mime_type)), + ], + ) +} + +pub type ProfileInput { + ProfileInput( + display_name: Option(String), + avatar: Option(BlobInput), + interests: Option(List(String)), + ) +} + +fn profile_input_to_json(input: ProfileInput) -> json.Json { + json.object( + [ + #("displayName", json.nullable(input.display_name, json.string)), + #("avatar", json.nullable(input.avatar, blob_input_to_json)), + #("interests", json.nullable( + input.interests, + fn(list) { json.array(from: list, of: json.string) }, + )), + ], + ) +} + +pub type Profile { + Profile(id: String, display_name: Option(String)) +} + +pub fn profile_decoder() -> decode.Decoder(Profile) { + use id <- decode.field("id", decode.string) + use display_name <- decode.field("displayName", decode.optional(decode.string)) + decode.success(Profile(id: id, display_name: display_name)) +} + +pub type UpdateProfileResponse { + UpdateProfileResponse(update_profile: Option(Profile)) +} + +pub fn update_profile_response_decoder() -> decode.Decoder(UpdateProfileResponse) { + use update_profile <- decode.field("updateProfile", decode.optional(profile_decoder())) + decode.success(UpdateProfileResponse(update_profile: update_profile)) +} + +pub fn update_profile(client: squall.Client, input: ProfileInput) -> Result(UpdateProfileResponse, String) { + let query = + "mutation UpdateProfile($input: ProfileInput!) { updateProfile(input: $input) { id displayName } }" + let variables = + json.object([#("input", profile_input_to_json(input))]) + let body = + json.object([#("query", json.string(query)), #("variables", variables)]) + use req <- result.try( + request.to(client.endpoint) + |> result.map_error(fn(_) { "Invalid endpoint URL" }), + ) + let req = + req + |> request.set_method(http.Post) + |> request.set_body(json.to_string(body)) + |> request.set_header("content-type", "application/json") + let req = + list.fold(client.headers, req, fn(r, header) { + request.set_header(r, header.0, header.1) + }) + use resp <- result.try( + httpc.send(req) + |> result.map_error(fn(_) { "HTTP request failed" }), + ) + use json_value <- result.try( + json.parse(from: resp.body, using: decode.dynamic) + |> result.map_error(fn(_) { "Failed to decode JSON response" }), + ) + let data_and_response_decoder = { + use data <- decode.field("data", update_profile_response_decoder()) + decode.success(data) + } + decode.run(json_value, data_and_response_decoder) + |> result.map_error(fn(_) { "Failed to decode response data" }) +} + diff --git a/birdie_snapshots/query_with_inline_array_arguments.accepted b/birdie_snapshots/query_with_inline_array_arguments.accepted index 8425879..1d52613 100644 --- a/birdie_snapshots/query_with_inline_array_arguments.accepted +++ b/birdie_snapshots/query_with_inline_array_arguments.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Query with inline array arguments +file: ./test/codegen_test.gleam +test_name: generate_inline_array_arguments_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/query_with_inline_object_arguments.accepted b/birdie_snapshots/query_with_inline_object_arguments.accepted index 2e998e2..0301497 100644 --- a/birdie_snapshots/query_with_inline_object_arguments.accepted +++ b/birdie_snapshots/query_with_inline_object_arguments.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Query with inline object arguments +file: ./test/codegen_test.gleam +test_name: generate_inline_object_arguments_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/query_with_inline_scalar_arguments.accepted b/birdie_snapshots/query_with_inline_scalar_arguments.accepted index 6bd5efa..d74e789 100644 --- a/birdie_snapshots/query_with_inline_scalar_arguments.accepted +++ b/birdie_snapshots/query_with_inline_scalar_arguments.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Query with inline scalar arguments +file: ./test/codegen_test.gleam +test_name: generate_inline_scalar_arguments_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted b/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted index a9f585e..a5c6b41 100644 --- a/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted +++ b/birdie_snapshots/query_with_multiple_root_fields_and_mixed_arguments.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Query with multiple root fields and mixed arguments +file: ./test/codegen_test.gleam +test_name: generate_multiple_root_fields_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/query_with_nested_types_generation.accepted b/birdie_snapshots/query_with_nested_types_generation.accepted index cfd8dd0..e6c1fa3 100644 --- a/birdie_snapshots/query_with_nested_types_generation.accepted +++ b/birdie_snapshots/query_with_nested_types_generation.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Query with nested types generation +file: ./test/codegen_test.gleam +test_name: generate_query_with_nested_types_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/query_with_variables_function_generation.accepted b/birdie_snapshots/query_with_variables_function_generation.accepted index 9747ec3..284d0f5 100644 --- a/birdie_snapshots/query_with_variables_function_generation.accepted +++ b/birdie_snapshots/query_with_variables_function_generation.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Query with variables function generation +file: ./test/codegen_test.gleam +test_name: generate_query_with_variables_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/simple_query_function_generation.accepted b/birdie_snapshots/simple_query_function_generation.accepted index c74c87b..a7949b4 100644 --- a/birdie_snapshots/simple_query_function_generation.accepted +++ b/birdie_snapshots/simple_query_function_generation.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Simple query function generation +file: ./test/codegen_test.gleam +test_name: generate_simple_query_test --- import gleam/dynamic/decode import gleam/http diff --git a/birdie_snapshots/type_with_reserved_keywords.accepted b/birdie_snapshots/type_with_reserved_keywords.accepted index 44b0e9d..7163758 100644 --- a/birdie_snapshots/type_with_reserved_keywords.accepted +++ b/birdie_snapshots/type_with_reserved_keywords.accepted @@ -1,6 +1,8 @@ --- version: 1.4.1 title: Type with reserved keywords +file: ./test/codegen_test.gleam +test_name: generate_with_reserved_keywords_test --- import gleam/dynamic/decode import gleam/http diff --git a/src/squall/internal/codegen.gleam b/src/squall/internal/codegen.gleam index 7d8a128..71972c4 100644 --- a/src/squall/internal/codegen.gleam +++ b/src/squall/internal/codegen.gleam @@ -28,6 +28,15 @@ type NestedTypeInfo { ) } +// Type to track input types that need to be generated +type InputTypeInfo { + InputTypeInfo( + type_name: String, + input_fields: List(schema.InputValue), + field_types: dict.Dict(String, schema.Type), + ) +} + // --- CONSTANTS --------------------------------------------------------------- const indent = 2 @@ -199,21 +208,39 @@ pub fn generate_operation( let decoder = generate_decoder_with_schema(response_type_name, field_types, schema_data.types) - // Generate function + // Collect Input types from variables let variables = parser.get_variables(operation) + use input_types <- result.try(collect_input_types(variables, schema_data.types)) + + // Generate Input type definitions and serializers + let input_docs = + input_types + |> list.map(fn(input_info) { + let type_doc = generate_input_type_definition(input_info) + let serializer_doc = generate_input_serializer(input_info) + [type_doc, serializer_doc] + }) + |> list.flatten + + // Generate function let function_def = generate_function( operation_name, response_type_name, variables, build_query_string(operation), + schema_data.types, ) // Build imports let imports = imports_doc() // Combine all code using doc combinators - let all_docs = [imports, ..nested_docs] |> list.append([type_def, decoder, function_def]) + // Order: imports, input types, nested types, response type, response decoder, function + let all_docs = + [imports, ..input_docs] + |> list.append(nested_docs) + |> list.append([type_def, decoder, function_def]) let code = all_docs |> doc.join(with: doc.lines(2)) @@ -324,6 +351,100 @@ fn get_base_type_name(type_ref: schema.TypeRef) -> String { } } +// Collect all InputObject types used in variables +fn collect_input_types( + variables: List(parser.Variable), + schema_types: dict.Dict(String, schema.Type), +) -> Result(List(InputTypeInfo), Error) { + variables + |> list.try_map(fn(var) { + use schema_type_ref <- result.try( + type_mapping.parser_type_to_schema_type_with_schema( + var.type_ref, + schema_types, + ), + ) + collect_input_types_from_type_ref(schema_type_ref, schema_types, []) + }) + |> result.map(list.flatten) + |> result.map(fn(input_types) { + // Deduplicate by type name + input_types + |> list.fold(dict.new(), fn(acc, info) { + dict.insert(acc, info.type_name, info) + }) + |> dict.values + }) +} + +// Recursively collect InputObject types from a type reference +fn collect_input_types_from_type_ref( + type_ref: schema.TypeRef, + schema_types: dict.Dict(String, schema.Type), + collected: List(InputTypeInfo), +) -> Result(List(InputTypeInfo), Error) { + case type_ref { + schema.NamedType(name, kind) -> { + case kind { + schema.InputObject -> { + // Check if we've already collected this type (avoid infinite recursion) + let already_collected = + list.any(collected, fn(info) { info.type_name == name }) + + case already_collected { + True -> Ok(collected) + False -> { + // Look up the InputObject type in schema + use input_type <- result.try( + dict.get(schema_types, name) + |> result.map_error(fn(_) { + error.InvalidSchemaResponse("InputObject type not found: " <> name) + }), + ) + + case input_type { + schema.InputObjectType(_, input_fields, _) -> { + // Create InputTypeInfo + let info = + InputTypeInfo( + type_name: name, + input_fields: input_fields, + field_types: schema_types, + ) + + // Recursively collect nested InputObject types + use nested <- result.try( + input_fields + |> list.try_map(fn(field) { + collect_input_types_from_type_ref( + field.type_ref, + schema_types, + [info, ..collected], + ) + }) + |> result.map(list.flatten), + ) + + Ok([info, ..nested]) + } + _ -> + Error(error.InvalidSchemaResponse( + "Expected InputObject type: " <> name, + )) + } + } + } + } + _ -> Ok(collected) + } + } + schema.NonNullType(inner) -> + collect_input_types_from_type_ref(inner, schema_types, collected) + schema.ListType(inner) -> + collect_input_types_from_type_ref(inner, schema_types, collected) + } +} + // Generate type definition fn generate_type_definition( type_name: String, @@ -492,12 +613,176 @@ fn generate_field_decoder(gleam_type: type_mapping.GleamType) -> String { } } +// Generate Input type definition +fn generate_input_type_definition(input_info: InputTypeInfo) -> Document { + let field_docs = + input_info.input_fields + |> list.map(fn(input_value) { + let sanitized_name = sanitize_field_name(input_value.name) + use gleam_type <- result.try(type_mapping.graphql_to_gleam_nullable( + input_value.type_ref, + )) + let field_doc = + doc.concat([ + doc.from_string(sanitized_name <> ": "), + doc.from_string(type_mapping.to_gleam_type_string(gleam_type)), + ]) + Ok(field_doc) + }) + |> list.filter_map(fn(r) { r }) + + [ + doc.from_string("pub type " <> input_info.type_name <> " {"), + [ + doc.line, + call_doc(input_info.type_name, field_docs), + ] + |> doc.concat + |> doc.nest(by: indent), + doc.line, + doc.from_string("}"), + ] + |> doc.concat + |> doc.group +} + +// Generate Input serializer function +fn generate_input_serializer(input_info: InputTypeInfo) -> Document { + let serializer_name = snake_case(input_info.type_name) <> "_to_json" + let param_name = "input" + + let field_entries = + input_info.input_fields + |> list.map(fn(input_value) { + let sanitized_name = sanitize_field_name(input_value.name) + use gleam_type <- result.try(type_mapping.graphql_to_gleam_nullable( + input_value.type_ref, + )) + + let value_expr = + encode_input_field_value( + param_name <> "." <> sanitized_name, + gleam_type, + input_value.type_ref, + input_info.field_types, + ) + + Ok( + doc.concat([ + doc.from_string("#("), + string_doc(input_value.name), + doc.from_string(", "), + value_expr, + doc.from_string(")"), + ]), + ) + }) + |> list.filter_map(fn(r) { r }) + + let body = + call_doc("json.object", [comma_list("[", field_entries, "]")]) + + doc.concat([ + doc.from_string("fn " <> serializer_name <> "("), + doc.from_string(param_name <> ": " <> input_info.type_name), + doc.from_string(") -> json.Json "), + block([body]), + ]) +} + +// Encode a field value for Input serialization +fn encode_input_field_value( + field_access: String, + gleam_type: type_mapping.GleamType, + type_ref: schema.TypeRef, + schema_types: dict.Dict(String, schema.Type), +) -> Document { + case gleam_type { + type_mapping.StringType -> + call_doc("json.string", [doc.from_string(field_access)]) + type_mapping.IntType -> + call_doc("json.int", [doc.from_string(field_access)]) + type_mapping.FloatType -> + call_doc("json.float", [doc.from_string(field_access)]) + type_mapping.BoolType -> + call_doc("json.bool", [doc.from_string(field_access)]) + type_mapping.ListType(inner) -> { + let base_type_name = get_base_type_name(type_ref) + case dict.get(schema_types, base_type_name) { + Ok(schema.InputObjectType(_, _, _)) -> { + // List of InputObjects + call_doc("json.array", [ + doc.from_string("from: " <> field_access), + doc.from_string("of: " <> snake_case(base_type_name) <> "_to_json"), + ]) + } + _ -> { + // List of scalars + let of_fn = case inner { + type_mapping.StringType -> "json.string" + type_mapping.IntType -> "json.int" + type_mapping.FloatType -> "json.float" + type_mapping.BoolType -> "json.bool" + _ -> "json.string" + } + call_doc("json.array", [ + doc.from_string("from: " <> field_access), + doc.from_string("of: " <> of_fn), + ]) + } + } + } + type_mapping.OptionType(inner) -> { + let base_type_name = get_base_type_name(type_ref) + case dict.get(schema_types, base_type_name) { + Ok(schema.InputObjectType(_, _, _)) -> { + // Optional InputObject + call_doc("json.nullable", [ + doc.from_string(field_access), + doc.from_string(snake_case(base_type_name) <> "_to_json"), + ]) + } + _ -> { + // Optional scalar or list + let inner_encoder = case inner { + type_mapping.StringType -> "json.string" + type_mapping.IntType -> "json.int" + type_mapping.FloatType -> "json.float" + type_mapping.BoolType -> "json.bool" + type_mapping.ListType(_) -> { + // This is handled by recursion, but for now use a lambda + let of_fn = case inner { + type_mapping.ListType(type_mapping.StringType) -> "json.string" + type_mapping.ListType(type_mapping.IntType) -> "json.int" + type_mapping.ListType(type_mapping.FloatType) -> "json.float" + type_mapping.ListType(type_mapping.BoolType) -> "json.bool" + _ -> "json.string" + } + "fn(list) { json.array(from: list, of: " <> of_fn <> ") }" + } + _ -> "json.string" + } + call_doc("json.nullable", [ + doc.from_string(field_access), + doc.from_string(inner_encoder), + ]) + } + } + } + type_mapping.CustomType(name) -> { + // This is an InputObject + call_doc(snake_case(name) <> "_to_json", [doc.from_string(field_access)]) + } + } +} + // Generate function fn generate_function( operation_name: String, response_type_name: String, variables: List(parser.Variable), query_string: String, + schema_types: dict.Dict(String, schema.Type), ) -> Document { let function_name = operation_name @@ -508,10 +793,15 @@ fn generate_function( let var_param_docs = vars |> list.map(fn(var) { - use gleam_type <- result.try( - type_mapping.parser_type_to_schema_type(var.type_ref) - |> result.try(type_mapping.graphql_to_gleam), + use schema_type_ref <- result.try( + type_mapping.parser_type_to_schema_type_with_schema( + var.type_ref, + schema_types, + ), ) + use gleam_type <- result.try(type_mapping.graphql_to_gleam( + schema_type_ref, + )) let param_name = snake_case(var.name) Ok( doc.from_string( @@ -532,12 +822,23 @@ fn generate_function( let var_entry_docs = vars |> list.map(fn(var) { - use gleam_type <- result.try( - type_mapping.parser_type_to_schema_type(var.type_ref) - |> result.try(type_mapping.graphql_to_gleam), + use schema_type_ref <- result.try( + type_mapping.parser_type_to_schema_type_with_schema( + var.type_ref, + schema_types, + ), ) + use gleam_type <- result.try(type_mapping.graphql_to_gleam( + schema_type_ref, + )) let param_name = snake_case(var.name) - let value_encoder = encode_variable_value(param_name, gleam_type) + let value_encoder = + encode_variable_value( + param_name, + gleam_type, + schema_type_ref, + schema_types, + ) Ok( doc.concat([ doc.from_string("#("), @@ -667,6 +968,8 @@ fn generate_function( fn encode_variable_value( var_name: String, gleam_type: type_mapping.GleamType, + type_ref: schema.TypeRef, + schema_types: dict.Dict(String, schema.Type), ) -> Document { case gleam_type { type_mapping.StringType -> @@ -676,25 +979,62 @@ fn encode_variable_value( call_doc("json.float", [doc.from_string(var_name)]) type_mapping.BoolType -> call_doc("json.bool", [doc.from_string(var_name)]) type_mapping.ListType(inner) -> { - let encoder = case inner { - type_mapping.StringType -> "json.string" - type_mapping.IntType -> "json.int" - type_mapping.FloatType -> "json.float" - type_mapping.BoolType -> "json.bool" - _ -> "json.string" + let base_type_name = get_base_type_name(type_ref) + case dict.get(schema_types, base_type_name) { + Ok(schema.InputObjectType(_, _, _)) -> { + // List of InputObjects + call_doc("json.array", [ + doc.from_string("from: " <> var_name), + doc.from_string("of: " <> snake_case(base_type_name) <> "_to_json"), + ]) + } + _ -> { + // List of scalars + let encoder = case inner { + type_mapping.StringType -> "json.string" + type_mapping.IntType -> "json.int" + type_mapping.FloatType -> "json.float" + type_mapping.BoolType -> "json.bool" + _ -> "json.string" + } + call_doc("json.array", [ + doc.from_string("from: " <> var_name), + doc.from_string("of: " <> encoder), + ]) + } + } + } + type_mapping.OptionType(inner) -> { + let base_type_name = get_base_type_name(type_ref) + case dict.get(schema_types, base_type_name) { + Ok(schema.InputObjectType(_, _, _)) -> { + // Optional InputObject + call_doc("json.nullable", [ + doc.from_string(var_name), + doc.from_string(snake_case(base_type_name) <> "_to_json"), + ]) + } + _ -> { + // Optional scalar or list - need to unwrap the type_ref + let inner_type_ref = case type_ref { + schema.NonNullType(t) -> t + t -> t + } + call_doc("json.nullable", [ + doc.from_string(var_name), + encode_variable_value("value", inner, inner_type_ref, schema_types), + ]) + } + } + } + type_mapping.CustomType(name) -> { + // Check if this is an InputObject + case dict.get(schema_types, name) { + Ok(schema.InputObjectType(_, _, _)) -> + call_doc(snake_case(name) <> "_to_json", [doc.from_string(var_name)]) + _ -> call_doc("json.string", [doc.from_string(var_name)]) } - call_doc("json.array", [ - doc.from_string("from: " <> var_name), - doc.from_string("of: " <> encoder), - ]) } - type_mapping.OptionType(inner) -> - call_doc("json.nullable", [ - doc.from_string(var_name), - encode_variable_value("value", inner), - ]) - type_mapping.CustomType(_) -> - call_doc("json.string", [doc.from_string(var_name)]) } } diff --git a/src/squall/internal/type_mapping.gleam b/src/squall/internal/type_mapping.gleam index 7c25913..3deab86 100644 --- a/src/squall/internal/type_mapping.gleam +++ b/src/squall/internal/type_mapping.gleam @@ -1,3 +1,4 @@ +import gleam/dict import gleam/option.{type Option, None, Some} import gleam/result import squall/internal/error.{type Error} @@ -83,6 +84,42 @@ pub fn parser_type_to_schema_type( } } +// Convert parser TypeRef to schema TypeRef with schema lookup for accurate kinds +pub fn parser_type_to_schema_type_with_schema( + parser_type: parser.TypeRef, + schema_types: dict.Dict(String, schema.Type), +) -> Result(schema.TypeRef, Error) { + case parser_type { + parser.NamedTypeRef(name) -> { + // Look up the type in schema to get the actual kind + let kind = case dict.get(schema_types, name) { + Ok(schema.ScalarType(_, _)) -> schema.Scalar + Ok(schema.ObjectType(_, _, _)) -> schema.Object + Ok(schema.InterfaceType(_, _, _)) -> schema.Interface + Ok(schema.UnionType(_, _, _)) -> schema.Union + Ok(schema.EnumType(_, _, _)) -> schema.Enum + Ok(schema.InputObjectType(_, _, _)) -> schema.InputObject + Error(_) -> schema.Scalar + } + Ok(schema.NamedType(name, kind)) + } + parser.ListTypeRef(inner) -> { + use inner_schema <- result.try(parser_type_to_schema_type_with_schema( + inner, + schema_types, + )) + Ok(schema.ListType(inner_schema)) + } + parser.NonNullTypeRef(inner) -> { + use inner_schema <- result.try(parser_type_to_schema_type_with_schema( + inner, + schema_types, + )) + Ok(schema.NonNullType(inner_schema)) + } + } +} + // Type checking helpers pub fn is_string_type(gleam_type: GleamType) -> Bool { diff --git a/test/codegen_test.gleam b/test/codegen_test.gleam index cac8871..9e8ec4b 100644 --- a/test/codegen_test.gleam +++ b/test/codegen_test.gleam @@ -720,3 +720,214 @@ pub fn generate_multiple_root_fields_test() { Error(_) -> Nil } } + +// Test: Generate mutation with InputObject variable +pub fn generate_mutation_with_input_object_test() { + let mutation_source = + " + mutation UpdateProfile($input: ProfileInput!) { + updateProfile(input: $input) { + id + displayName + } + } + " + + let assert Ok(operation) = parser.parse(mutation_source) + + // Define InputObject type in schema + let profile_input_fields = [ + schema.InputValue( + "displayName", + schema.NamedType("String", schema.Scalar), + None, + ), + schema.InputValue( + "description", + schema.NamedType("String", schema.Scalar), + None, + ), + ] + + let profile_fields = [ + schema.Field( + "id", + schema.NonNullType(schema.NamedType("ID", schema.Scalar)), + [], + None, + ), + schema.Field( + "displayName", + schema.NamedType("String", schema.Scalar), + [], + None, + ), + ] + + let mock_schema = + schema.Schema( + Some("Query"), + Some("Mutation"), + None, + dict.from_list([ + #("Profile", schema.ObjectType("Profile", profile_fields, None)), + #( + "ProfileInput", + schema.InputObjectType("ProfileInput", profile_input_fields, None), + ), + #( + "Mutation", + schema.ObjectType( + "Mutation", + [ + schema.Field( + "updateProfile", + schema.NamedType("Profile", schema.Object), + [ + schema.InputValue( + "input", + schema.NonNullType(schema.NamedType( + "ProfileInput", + schema.InputObject, + )), + None, + ), + ], + None, + ), + ], + None, + ), + ), + ]), + ) + + let result = + codegen.generate_operation("update_profile", operation, mock_schema, "") + + case result { + Ok(code) -> { + code + |> birdie.snap(title: "Mutation with InputObject variable") + } + Error(_) -> Nil + } +} + +// Test: Generate mutation with nested InputObject types +pub fn generate_mutation_with_nested_input_object_test() { + let mutation_source = + " + mutation UpdateProfile($input: ProfileInput!) { + updateProfile(input: $input) { + id + displayName + } + } + " + + let assert Ok(operation) = parser.parse(mutation_source) + + // Define nested InputObject types + let blob_input_fields = [ + schema.InputValue( + "data", + schema.NonNullType(schema.NamedType("String", schema.Scalar)), + None, + ), + schema.InputValue( + "mimeType", + schema.NonNullType(schema.NamedType("String", schema.Scalar)), + None, + ), + ] + + let profile_input_fields = [ + schema.InputValue( + "displayName", + schema.NamedType("String", schema.Scalar), + None, + ), + schema.InputValue( + "avatar", + schema.NamedType("BlobInput", schema.InputObject), + None, + ), + schema.InputValue( + "interests", + schema.ListType(schema.NonNullType(schema.NamedType( + "String", + schema.Scalar, + ))), + None, + ), + ] + + let profile_fields = [ + schema.Field( + "id", + schema.NonNullType(schema.NamedType("ID", schema.Scalar)), + [], + None, + ), + schema.Field( + "displayName", + schema.NamedType("String", schema.Scalar), + [], + None, + ), + ] + + let mock_schema = + schema.Schema( + Some("Query"), + Some("Mutation"), + None, + dict.from_list([ + #("Profile", schema.ObjectType("Profile", profile_fields, None)), + #( + "ProfileInput", + schema.InputObjectType("ProfileInput", profile_input_fields, None), + ), + #( + "BlobInput", + schema.InputObjectType("BlobInput", blob_input_fields, None), + ), + #( + "Mutation", + schema.ObjectType( + "Mutation", + [ + schema.Field( + "updateProfile", + schema.NamedType("Profile", schema.Object), + [ + schema.InputValue( + "input", + schema.NonNullType(schema.NamedType( + "ProfileInput", + schema.InputObject, + )), + None, + ), + ], + None, + ), + ], + None, + ), + ), + ]), + ) + + let result = + codegen.generate_operation("update_profile", operation, mock_schema, "") + + case result { + Ok(code) -> { + code + |> birdie.snap(title: "Mutation with nested InputObject types") + } + Error(_) -> Nil + } +}