Class: Yogurt::CodeGenerator
- Inherits:
-
Object
- Object
- Yogurt::CodeGenerator
- Extended by:
- T::Sig
- Includes:
- Utils
- Defined in:
- lib/yogurt/code_generator.rb,
lib/yogurt/code_generator/utils.rb,
lib/yogurt/code_generator/enum_class.rb,
lib/yogurt/code_generator/leaf_class.rb,
lib/yogurt/code_generator/root_class.rb,
lib/yogurt/code_generator/input_class.rb,
lib/yogurt/code_generator/typed_input.rb,
lib/yogurt/code_generator/type_wrapper.rb,
lib/yogurt/code_generator/typed_output.rb,
lib/yogurt/code_generator/defined_class.rb,
lib/yogurt/code_generator/defined_method.rb,
lib/yogurt/code_generator/generated_file.rb,
lib/yogurt/code_generator/field_access_path.rb,
lib/yogurt/code_generator/field_access_method.rb,
lib/yogurt/code_generator/variable_definition.rb,
lib/yogurt/code_generator/defined_class_sorter.rb,
lib/yogurt/code_generator/operation_declaration.rb
Defined Under Namespace
Modules: DefinedClass, DefinedMethod, Utils Classes: DefinedClassSorter, EnumClass, FieldAccessMethod, FieldAccessPath, GeneratedFile, InputClass, LeafClass, OperationDeclaration, RootClass, TypeWrapper, TypedInput, TypedOutput, VariableDefinition
Constant Summary collapse
- PROTECTED_NAMES =
T.let([ *Object.instance_methods, *Yogurt::QueryResult.instance_methods, *Yogurt::ErrorResult.instance_methods ].map(&:to_s).sort.uniq.freeze, T::Array[String])
Instance Attribute Summary collapse
-
#classes ⇒ Object
readonly
Returns the value of attribute classes.
Instance Method Summary collapse
- #add_class(definition) ⇒ Object
- #build_expression(wrappers, variable_name, array_wrappers, level, core_expression) ⇒ Object
- #content_files ⇒ Object
- #contents ⇒ Object
- #deserializer_or_default(type_name, &block) ⇒ Object
- #ensure_constant_name(name) ⇒ Object
- #enum_class(enum_type) ⇒ Object
- #formatted_contents ⇒ Object
- #generate(declaration) ⇒ Object
- #generate_output_type(graphql_type, subselections, next_module_name, input_name, dependencies) ⇒ Object
-
#initialize(schema) ⇒ CodeGenerator
constructor
A new instance of CodeGenerator.
- #input_class(graphql_name) ⇒ Object
- #input_type_from_scalar_converter(scalar_converter) ⇒ Object
- #output_type_from_scalar_converter(scalar_converter) ⇒ Object
- #schema ⇒ Object
- #serializer_or_default(type_name, &block) ⇒ Object
- #unwrapped_graphql_type_to_input_type(graphql_type) ⇒ Object
- #unwrapped_graphql_type_to_output_type(graphql_type, subselections, next_module_name, dependencies) ⇒ Object
- #variable_definition(variable) ⇒ Object
Methods included from Utils
#camelize, #generate_method_name, #generate_pretty_print, #indent, #possible_types_constant, #typename_method, #underscore
Constructor Details
#initialize(schema) ⇒ CodeGenerator
Returns a new instance of CodeGenerator.
19 20 21 22 23 24 25 26 27 28 29 |
# File 'lib/yogurt/code_generator.rb', line 19 def initialize(schema) @schema = T.let(schema, GRAPHQL_SCHEMA) # Maps GraphQL enum name to class name @enums = T.let({}, T::Hash[String, String]) # Maps GraphQL input type name to class name @input_types = T.let({}, T::Hash[String, String]) @scalars = T.let(Yogurt.scalar_converters(schema), T::Hash[String, SCALAR_CONVERTER]) @classes = T.let({}, T::Hash[String, DefinedClass]) end |
Instance Attribute Details
#classes ⇒ Object (readonly)
Returns the value of attribute classes.
16 17 18 |
# File 'lib/yogurt/code_generator.rb', line 16 def classes @classes end |
Instance Method Details
#add_class(definition) ⇒ Object
116 117 118 119 120 |
# File 'lib/yogurt/code_generator.rb', line 116 def add_class(definition) raise "Attempting to redefine class #{definition.name}" if @classes.key?(definition.name) @classes[definition.name] = definition end |
#build_expression(wrappers, variable_name, array_wrappers, level, core_expression) ⇒ Object
329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 |
# File 'lib/yogurt/code_generator.rb', line 329 def build_expression(wrappers, variable_name, array_wrappers, level, core_expression) next_wrapper = wrappers.shift case next_wrapper when TypeWrapper::ARRAY array_wrappers -= 1 next_variable_name = if array_wrappers.zero? "raw_value" else "inner_value#{array_wrappers}" end indent(<<~STRING.rstrip, level.positive? ? 1 : 0) #{variable_name}.map do |#{next_variable_name}| #{indent(build_expression(wrappers, next_variable_name, array_wrappers, level + 1, core_expression), 1)} end STRING when TypeWrapper::NILABLE break_word = level.zero? ? 'return' : 'next' <<~STRING.rstrip #{break_word} if #{variable_name}.nil? #{build_expression(wrappers, variable_name, array_wrappers, level, core_expression)} STRING when nil if level.zero? core_expression.gsub(/raw_value/, variable_name) else core_expression end else T.absurd(next_wrapper) end end |
#content_files ⇒ Object
52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 |
# File 'lib/yogurt/code_generator.rb', line 52 def content_files DefinedClassSorter.new(classes.values).sorted_classes.map do |klass| GeneratedFile.new( constant_name: klass.name, dependencies: klass.dependencies, code: klass.to_ruby, type: case klass when RootClass GeneratedFile::FileType::OPERATION when LeafClass GeneratedFile::FileType::OBJECT_RESULT when InputClass GeneratedFile::FileType::INPUT_OBJECT when EnumClass GeneratedFile::FileType::ENUM else raise "Unhandled class type: #{klass.inspect}" end, ) end end |
#contents ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 |
# File 'lib/yogurt/code_generator.rb', line 33 def contents definitions = DefinedClassSorter.new(classes.values) .sorted_classes .map(&:to_ruby) .join("\n") <<~STRING # typed: strict # frozen_string_literal: true require 'pp' #{definitions} STRING end |
#deserializer_or_default(type_name, &block) ⇒ Object
443 444 445 446 447 448 |
# File 'lib/yogurt/code_generator.rb', line 443 def deserializer_or_default(type_name, &block) deserializer = @scalars[type_name] return output_type_from_scalar_converter(deserializer) if deserializer yield end |
#ensure_constant_name(name) ⇒ Object
128 129 130 131 132 |
# File 'lib/yogurt/code_generator.rb', line 128 def ensure_constant_name(name) return if name.match?(/\A[A-Z][a-zA-Z0-9_]+\z/) raise "You must use valid Ruby constant names for query names (got #{name})" end |
#enum_class(enum_type) ⇒ Object
135 136 137 138 139 140 141 142 143 |
# File 'lib/yogurt/code_generator.rb', line 135 def enum_class(enum_type) enum_class_name = @enums[enum_type.graphql_name] return enum_class_name if enum_class_name # TODO: sanitize the name klass_name = "::#{schema.name}::#{enum_type.graphql_name}" add_class(EnumClass.new(name: klass_name, serialized_values: enum_type.values.keys)) @enums[enum_type.graphql_name] = klass_name end |
#formatted_contents ⇒ Object
76 77 78 79 80 81 82 |
# File 'lib/yogurt/code_generator.rb', line 76 def formatted_contents if defined?(CodeRay) CodeRay.scan(contents, :ruby).term else contents end end |
#generate(declaration) ⇒ Object
85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 |
# File 'lib/yogurt/code_generator.rb', line 85 def generate(declaration) query = GraphQL::Query.new(declaration.schema, declaration.query_text) query.operations.each do |name, op_def| owner_type = case op_def.operation_type when 'query' schema.query when 'mutation' schema.mutation when 'subscription' schema.subscription else Kernel.raise("Unknown operation type: #{op_def.type}") end ensure_constant_name(name) module_name = "::#{declaration.container.name}::#{name}" generate_result_class( module_name, owner_type, op_def.selections, operation_declaration: OperationDeclaration.new( declaration: declaration, operation_name: name, variables: op_def.variables, ), ) end end |
#generate_output_type(graphql_type, subselections, next_module_name, input_name, dependencies) ⇒ Object
372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 |
# File 'lib/yogurt/code_generator.rb', line 372 def generate_output_type(graphql_type, subselections, next_module_name, input_name, dependencies) # Unwrap the graphql type, but keep track of the wrappers that it had # so that we can build the sorbet signature and return expression. wrappers = T.let([], T::Array[TypeWrapper]) fully_unwrapped_type = T.let(graphql_type, T.untyped) # Sorbet uses nullable wrappers, whereas GraphQL uses non-nullable wrappers. # This boolean is used to help with the conversion. skip_nilable = T.let(false, T::Boolean) array_wrappers = 0 loop do if fully_unwrapped_type.non_null? fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type skip_nilable = true next end wrappers << TypeWrapper::NILABLE if !skip_nilable skip_nilable = false if fully_unwrapped_type.list? wrappers << TypeWrapper::ARRAY array_wrappers += 1 fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type next end break end core_typed_expression = unwrapped_graphql_type_to_output_type(fully_unwrapped_type, subselections, next_module_name, dependencies) signature = core_typed_expression.signature variable_name = "raw_result[#{input_name.inspect}]" method_body = build_expression(wrappers.dup, variable_name, array_wrappers, 0, core_typed_expression.deserializer) wrappers.reverse_each do |wrapper| case wrapper when TypeWrapper::ARRAY signature = "T::Array[#{signature}]" when TypeWrapper::NILABLE signature = "T.nilable(#{signature})" else T.absurd(wrapper) end end TypedOutput.new( signature: signature, deserializer: method_body, ) end |
#input_class(graphql_name) ⇒ Object
146 147 148 149 150 151 152 153 154 155 156 157 158 159 |
# File 'lib/yogurt/code_generator.rb', line 146 def input_class(graphql_name) input_class_name = @input_types[graphql_name] return input_class_name if input_class_name klass_name = "::#{schema.name}::#{graphql_name}" graphql_type = schema.types[graphql_name] arguments = graphql_type.arguments.each_value.map do |argument| variable_definition(argument) end add_class(InputClass.new(name: klass_name, arguments: arguments)) @input_types[graphql_name] = klass_name end |
#input_type_from_scalar_converter(scalar_converter) ⇒ Object
604 605 606 607 608 609 610 611 612 |
# File 'lib/yogurt/code_generator.rb', line 604 def input_type_from_scalar_converter(scalar_converter) name = scalar_converter.name raise "Expected scalar deserializer to be assigned to a constant" if name.nil? TypedInput.new( signature: scalar_converter.type_alias.name, serializer: "#{name}.serialize(raw_value)", ) end |
#output_type_from_scalar_converter(scalar_converter) ⇒ Object
427 428 429 430 431 432 433 434 435 |
# File 'lib/yogurt/code_generator.rb', line 427 def output_type_from_scalar_converter(scalar_converter) name = scalar_converter.name raise "Expected scalar deserializer to be assigned to a constant" if name.nil? TypedOutput.new( signature: scalar_converter.type_alias.name, deserializer: "#{name}.deserialize(raw_value)", ) end |
#schema ⇒ Object
123 124 125 |
# File 'lib/yogurt/code_generator.rb', line 123 def schema @schema end |
#serializer_or_default(type_name, &block) ⇒ Object
620 621 622 623 624 625 |
# File 'lib/yogurt/code_generator.rb', line 620 def serializer_or_default(type_name, &block) deserializer = @scalars[type_name] return input_type_from_scalar_converter(deserializer) if deserializer yield end |
#unwrapped_graphql_type_to_input_type(graphql_type) ⇒ Object
630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 |
# File 'lib/yogurt/code_generator.rb', line 630 def unwrapped_graphql_type_to_input_type(graphql_type) graphql_type = schema.types[T.unsafe(graphql_type).name] if graphql_type.is_a?(GraphQL::Language::Nodes::TypeName) if graphql_type == GraphQL::Types::Boolean TypedInput.new( signature: "T::Boolean", serializer: "raw_value", ) elsif graphql_type == GraphQL::Types::BigInt serializer_or_default(T.unsafe(GraphQL::Types::BigInt).graphql_name) do TypedInput.new( signature: "Integer", serializer: "raw_value", ) end elsif graphql_type == GraphQL::Types::ID serializer_or_default('ID') do TypedInput.new( signature: "String", serializer: "raw_value", ) end elsif graphql_type == GraphQL::Types::ISO8601Date serializer_or_default(T.unsafe(GraphQL::Types::ISO8601Date).graphql_name) do input_type_from_scalar_converter(Converters::Date) end elsif graphql_type == GraphQL::Types::ISO8601DateTime serializer_or_default(T.unsafe(GraphQL::Types::ISO8601DateTime).graphql_name) do input_type_from_scalar_converter(Converters::Time) end elsif graphql_type == GraphQL::Types::Int TypedInput.new( signature: "Integer", serializer: "raw_value", ) elsif graphql_type == GraphQL::Types::Float TypedInput.new( signature: "Float", serializer: "raw_value", ) elsif graphql_type == GraphQL::Types::String TypedInput.new( signature: "String", serializer: "raw_value", ) elsif graphql_type.is_a?(Class) if graphql_type < GraphQL::Schema::Enum klass_name = enum_class(graphql_type) TypedInput.new( signature: klass_name, serializer: "raw_value.serialize", dependency: klass_name, ) elsif graphql_type < GraphQL::Schema::Scalar serializer_or_default(graphql_type.graphql_name) do TypedInput.new( signature: T.unsafe(Yogurt::SCALAR_TYPE).name, serializer: "raw_value", ) end elsif graphql_type < GraphQL::Schema::InputObject klass_name = input_class(T.unsafe(graphql_type).graphql_name) TypedInput.new( signature: klass_name, serializer: "raw_value.serialize", dependency: klass_name, ) else raise "Unknown GraphQL type: #{graphql_type.inspect}" end else raise "Unknown GraphQL type: #{graphql_type.inspect}" end end |
#unwrapped_graphql_type_to_output_type(graphql_type, subselections, next_module_name, dependencies) ⇒ Object
460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 |
# File 'lib/yogurt/code_generator.rb', line 460 def unwrapped_graphql_type_to_output_type(graphql_type, subselections, next_module_name, dependencies) # TODO: Once https://github.com/sorbet/sorbet/issues/649 is fixed, change the `cast`'s back to `let`'s if graphql_type == GraphQL::Types::Boolean TypedOutput.new( signature: "T::Boolean", deserializer: 'T.cast(raw_value, T::Boolean)', ) elsif graphql_type == GraphQL::Types::BigInt deserializer_or_default(T.unsafe(GraphQL::Types::BigInt).graphql_name) do TypedOutput.new( signature: "Integer", deserializer: 'T.cast(raw_value, T.any(String, Integer)).to_i', ) end elsif graphql_type == GraphQL::Types::ID deserializer_or_default('ID') do TypedOutput.new( signature: "String", deserializer: 'T.cast(raw_value, String)', ) end elsif graphql_type == GraphQL::Types::ISO8601Date deserializer_or_default(T.unsafe(GraphQL::Types::ISO8601Date).graphql_name) do output_type_from_scalar_converter(Converters::Date) end elsif graphql_type == GraphQL::Types::ISO8601DateTime deserializer_or_default(T.unsafe(GraphQL::Types::ISO8601DateTime).graphql_name) do output_type_from_scalar_converter(Converters::Time) end elsif graphql_type == GraphQL::Types::Int TypedOutput.new( signature: "Integer", deserializer: 'T.cast(raw_value, Integer)', ) elsif graphql_type == GraphQL::Types::Float TypedOutput.new( signature: "Float", deserializer: 'T.cast(raw_value, Float)', ) elsif graphql_type == GraphQL::Types::String TypedOutput.new( signature: "String", deserializer: 'T.cast(raw_value, String)', ) else type_kind = graphql_type.kind if type_kind.enum? klass_name = enum_class(graphql_type) dependencies.push(klass_name) TypedOutput.new( signature: klass_name, deserializer: "#{klass_name}.deserialize(raw_value)", ) elsif type_kind.scalar? deserializer_or_default(graphql_type.graphql_name) do TypedOutput.new( signature: T.unsafe(Yogurt::SCALAR_TYPE).name, deserializer: "T.cast(raw_value, #{T.unsafe(Yogurt::SCALAR_TYPE).name})", ) end elsif type_kind.composite? generate_result_class( next_module_name, graphql_type, subselections, dependencies: dependencies, ) else raise "Unknown GraphQL type kind: #{graphql_type.graphql_name} (#{graphql_type.kind.inspect})" end end end |
#variable_definition(variable) ⇒ Object
535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 |
# File 'lib/yogurt/code_generator.rb', line 535 def variable_definition(variable) wrappers = T.let([], T::Array[TypeWrapper]) fully_unwrapped_type = T.let(variable.type, T.untyped) skip_nilable = T.let(false, T::Boolean) array_wrappers = 0 loop do non_null = fully_unwrapped_type.is_a?(GraphQL::Schema::NonNull) || fully_unwrapped_type.is_a?(GraphQL::Language::Nodes::NonNullType) if non_null fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type skip_nilable = true next end wrappers << TypeWrapper::NILABLE if !skip_nilable skip_nilable = false list = fully_unwrapped_type.is_a?(GraphQL::Schema::List) || fully_unwrapped_type.is_a?(GraphQL::Language::Nodes::ListType) if list wrappers << TypeWrapper::ARRAY array_wrappers += 1 fully_unwrapped_type = T.unsafe(fully_unwrapped_type).of_type next end break end core_input_type = unwrapped_graphql_type_to_input_type(fully_unwrapped_type) variable_name = underscore(variable.name).to_sym signature = core_input_type.signature serializer = core_input_type.serializer wrappers.reverse_each do |wrapper| case wrapper when TypeWrapper::NILABLE signature = "T.nilable(#{signature})" serializer = <<~STRING if raw_value #{indent(serializer, 1).strip} end STRING when TypeWrapper::ARRAY signature = "T::Array[#{signature}]" intermediate_name = "#{variable_name}#{array_wrappers}" serializer = serializer.gsub(/\braw_value\b/, intermediate_name) serializer = <<~STRING raw_value.map do |#{intermediate_name}| #{indent(serializer, 1).strip} end STRING else T.absurd(wrapper) end end serializer = serializer.gsub(/\braw_value\b/, variable_name.to_s) VariableDefinition.new( name: variable_name, graphql_name: variable.name, signature: signature, serializer: serializer.strip, dependency: core_input_type.dependency, ) end |