diff options
Diffstat (limited to 'lib/gitlab/graphql/docs/helper.rb')
-rw-r--r-- | lib/gitlab/graphql/docs/helper.rb | 434 |
1 files changed, 0 insertions, 434 deletions
diff --git a/lib/gitlab/graphql/docs/helper.rb b/lib/gitlab/graphql/docs/helper.rb deleted file mode 100644 index b598b605141..00000000000 --- a/lib/gitlab/graphql/docs/helper.rb +++ /dev/null @@ -1,434 +0,0 @@ -# frozen_string_literal: true - -return if Rails.env.production? - -module Gitlab - module Graphql - module Docs - # We assume a few things about the schema. We use the graphql-ruby gem, which enforces: - # - All mutations have a single input field named 'input' - # - All mutations have a payload type, named after themselves - # - All mutations have an input type, named after themselves - # If these things change, then some of this code will break. Such places - # are guarded with an assertion that our assumptions are not violated. - ViolatedAssumption = Class.new(StandardError) - - SUGGESTED_ACTION = <<~MSG - We expect it to be impossible to violate our assumptions about - how mutation arguments work. - - If that is not the case, then something has probably changed in the - way we generate our schema, perhaps in the library we use: graphql-ruby - - Please ask for help in the #f_graphql or #backend channels. - MSG - - CONNECTION_ARGS = %w[after before first last].to_set - - FIELD_HEADER = <<~MD - #### Fields - - | Name | Type | Description | - | ---- | ---- | ----------- | - MD - - ARG_HEADER = <<~MD - # Arguments - - | Name | Type | Description | - | ---- | ---- | ----------- | - MD - - CONNECTION_NOTE = <<~MD - This field returns a [connection](#connections). It accepts the - four standard [pagination arguments](#connection-pagination-arguments): - `before: String`, `after: String`, `first: Int`, `last: Int`. - MD - - # Helper with functions to be used by HAML templates - # This includes graphql-docs gem helpers class. - # You can check the included module on: https://github.com/gjtorikian/graphql-docs/blob/v1.6.0/lib/graphql-docs/helpers.rb - module Helper - include GraphQLDocs::Helpers - include Gitlab::Utils::StrongMemoize - - def auto_generated_comment - <<-MD.strip_heredoc - --- - stage: Plan - group: Project Management - info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers - --- - - <!--- - This documentation is auto generated by a script. - - Please do not edit this file directly, check compile_docs task on lib/tasks/gitlab/graphql.rake. - ---> - MD - end - - # Template methods: - # Methods that return chunks of Markdown for insertion into the document - - def render_full_field(field, heading_level: 3, owner: nil) - conn = connection?(field) - args = field[:arguments].reject { |arg| conn && CONNECTION_ARGS.include?(arg[:name]) } - arg_owner = [owner, field[:name]] - - chunks = [ - render_name_and_description(field, level: heading_level, owner: owner), - render_return_type(field), - render_input_type(field), - render_connection_note(field), - render_argument_table(heading_level, args, arg_owner), - render_return_fields(field, owner: owner) - ] - - join(:block, chunks) - end - - def render_argument_table(level, args, owner) - arg_header = ('#' * level) + ARG_HEADER - render_field_table(arg_header, args, owner) - end - - def render_name_and_description(object, owner: nil, level: 3) - content = [] - - heading = '#' * level - name = [owner, object[:name]].compact.join('.') - - content << "#{heading} `#{name}`" - content << render_description(object, owner, :block) - - join(:block, content) - end - - def render_object_fields(fields, owner:, level_bump: 0) - return if fields.blank? - - (with_args, no_args) = fields.partition { |f| args?(f) } - type_name = owner[:name] if owner - header_prefix = '#' * level_bump - sections = [ - render_simple_fields(no_args, type_name, header_prefix), - render_fields_with_arguments(with_args, type_name, header_prefix) - ] - - join(:block, sections) - end - - def render_enum_value(enum, value) - render_row(render_name(value, enum[:name]), render_description(value, enum[:name], :inline)) - end - - def render_union_member(member) - "- [`#{member}`](##{member.downcase})" - end - - # QUERIES: - - # Methods that return parts of the schema, or related information: - - def connection_object_types - objects.select { |t| t[:is_edge] || t[:is_connection] } - end - - def object_types - objects.reject { |t| t[:is_edge] || t[:is_connection] || t[:is_payload] } - end - - def interfaces - graphql_interface_types.map { |t| t.merge(fields: t[:fields] + t[:connections]) } - end - - def fields_of(type_name) - graphql_operation_types - .find { |type| type[:name] == type_name } - .values_at(:fields, :connections) - .flatten - .then { |fields| sorted_by_name(fields) } - end - - # Place the arguments of the input types on the mutation itself. - # see: `#input_types` - this method must not call `#input_types` to avoid mutual recursion - def mutations - @mutations ||= sorted_by_name(graphql_mutation_types).map do |t| - inputs = t[:input_fields] - input = inputs.first - name = t[:name] - - assert!(inputs.one?, "Expected exactly 1 input field named #{name}. Found #{inputs.count} instead.") - assert!(input[:name] == 'input', "Expected the input of #{name} to be named 'input'") - - input_type_name = input[:type][:name] - input_type = graphql_input_object_types.find { |t| t[:name] == input_type_name } - assert!(input_type.present?, "Cannot find #{input_type_name} for #{name}.input") - - arguments = input_type[:input_fields] - seen_type!(input_type_name) - t.merge(arguments: arguments) - end - end - - # We assume that the mutations have been processed first, marking their - # inputs as `seen_type?` - def input_types - mutations # ensure that mutations have seen their inputs first - graphql_input_object_types.reject { |t| seen_type?(t[:name]) } - end - - # We ignore the built-in enum types, and sort values by name - def enums - graphql_enum_types - .reject { |type| type[:values].empty? } - .reject { |enum_type| enum_type[:name].start_with?('__') } - .map { |type| type.merge(values: sorted_by_name(type[:values])) } - end - - private # DO NOT CALL THESE METHODS IN TEMPLATES - - # Template methods - - def render_return_type(query) - return unless query[:type] # for example, mutations - - "Returns #{render_field_type(query[:type])}." - end - - def render_simple_fields(fields, type_name, header_prefix) - render_field_table(header_prefix + FIELD_HEADER, fields, type_name) - end - - def render_fields_with_arguments(fields, type_name, header_prefix) - return if fields.empty? - - level = 5 + header_prefix.length - sections = sorted_by_name(fields).map do |f| - render_full_field(f, heading_level: level, owner: type_name) - end - - <<~MD.chomp - #{header_prefix}#### Fields with arguments - - #{join(:block, sections)} - MD - end - - def render_field_table(header, fields, owner) - return if fields.empty? - - fields = sorted_by_name(fields) - header + join(:table, fields.map { |f| render_field(f, owner) }) - end - - def render_field(field, owner) - render_row( - render_name(field, owner), - render_field_type(field[:type]), - render_description(field, owner, :inline) - ) - end - - def render_return_fields(mutation, owner:) - fields = mutation[:return_fields] - return if fields.blank? - - name = owner.to_s + mutation[:name] - render_object_fields(fields, owner: { name: name }) - end - - def render_connection_note(field) - return unless connection?(field) - - CONNECTION_NOTE.chomp - end - - def render_row(*values) - "| #{values.map { |val| val.to_s.squish }.join(' | ')} |" - end - - def render_name(object, owner = nil) - rendered_name = "`#{object[:name]}`" - rendered_name += ' **{warning-solid}**' if deprecated?(object, owner) - - return rendered_name unless owner - - owner = Array.wrap(owner).join('') - id = (owner + object[:name]).downcase - - %(<a id="#{id}"></a>) + rendered_name - end - - # Returns the object description. If the object has been deprecated, - # the deprecation reason will be returned in place of the description. - def render_description(object, owner = nil, context = :block) - if deprecated?(object, owner) - render_deprecation(object, owner, context) - else - render_description_of(object, owner, context) - end - end - - def deprecated?(object, owner) - return true if object[:is_deprecated] # only populated for fields, not arguments! - - key = [*Array.wrap(owner), object[:name]].join('.') - deprecations.key?(key) - end - - def render_description_of(object, owner, context = nil) - desc = if object[:is_edge] - base = object[:name].chomp('Edge') - "The edge type for [`#{base}`](##{base.downcase})." - elsif object[:is_connection] - base = object[:name].chomp('Connection') - "The connection type for [`#{base}`](##{base.downcase})." - else - object[:description]&.strip - end - - return if desc.blank? - - desc += '.' unless desc.ends_with?('.') - see = doc_reference(object, owner) - desc += " #{see}" if see - desc += " (see [Connections](#connections))" if connection?(object) && context != :block - desc - end - - def doc_reference(object, owner) - field = schema_field(owner, object[:name]) if owner - return unless field - - ref = field.try(:doc_reference) - return if ref.blank? - - parts = ref.to_a.map do |(title, url)| - "[#{title.strip}](#{url.strip})" - end - - "See #{parts.join(', ')}." - end - - def render_deprecation(object, owner, context) - buff = [] - deprecation = schema_deprecation(owner, object[:name]) - - buff << (deprecation&.original_description || render_description_of(object, owner)) if context == :block - buff << if deprecation - deprecation.markdown(context: context) - else - "**Deprecated:** #{object[:deprecation_reason]}" - end - - join(context, buff) - end - - def render_field_type(type) - "[`#{type[:info]}`](##{type[:name].downcase})" - end - - def join(context, chunks) - chunks.compact! - return if chunks.blank? - - case context - when :block - chunks.join("\n\n") - when :inline - chunks.join(" ").squish.presence - when :table - chunks.join("\n") - end - end - - # Queries - - def sorted_by_name(objects) - return [] unless objects.present? - - objects.sort_by { |o| o[:name] } - end - - def connection?(field) - type_name = field.dig(:type, :name) - type_name.present? && type_name.ends_with?('Connection') - end - - # We are ignoring connections and built in types for now, - # they should be added when queries are generated. - def objects - strong_memoize(:objects) do - mutations = schema.mutation&.fields&.keys&.to_set || [] - - graphql_object_types - .reject { |object_type| object_type[:name]["__"] || object_type[:name] == 'Subscription' } # We ignore introspection and subscription types. - .map do |type| - name = type[:name] - type.merge( - is_edge: name.ends_with?('Edge'), - is_connection: name.ends_with?('Connection'), - is_payload: name.ends_with?('Payload') && mutations.include?(name.chomp('Payload').camelcase(:lower)), - fields: type[:fields] + type[:connections] - ) - end - end - end - - def args?(field) - args = field[:arguments] - return false if args.blank? - return true unless connection?(field) - - args.any? { |arg| CONNECTION_ARGS.exclude?(arg[:name]) } - end - - # returns the deprecation information for a field or argument - # See: Gitlab::Graphql::Deprecation - def schema_deprecation(type_name, field_name) - key = [*Array.wrap(type_name), field_name].join('.') - deprecations[key] - end - - def render_input_type(query) - input_field = query[:input_fields]&.first - return unless input_field - - "Input type: `#{input_field[:type][:name]}`" - end - - def schema_field(type_name, field_name) - type = schema.types[type_name] - return unless type && type.kind.fields? - - type.fields[field_name] - end - - def deprecations - strong_memoize(:deprecations) do - mapping = {} - - schema.types.each do |type_name, type| - next unless type.kind.fields? - - type.fields.each do |field_name, field| - mapping["#{type_name}.#{field_name}"] = field.try(:deprecation) - field.arguments.each do |arg_name, arg| - mapping["#{type_name}.#{field_name}.#{arg_name}"] = arg.try(:deprecation) - end - end - end - - mapping.compact - end - end - - def assert!(claim, message) - raise ViolatedAssumption, "#{message}\n#{SUGGESTED_ACTION}" unless claim - end - end - end - end -end |