Class: Gitlab::GrapeOpenapi::Converters::CrossFieldValidationResolver

Inherits:
Object
  • Object
show all
Defined in:
lib/gitlab/grape_openapi/converters/cross_field_validation_resolver.rb

Constant Summary collapse

MUTUALLY_EXCLUSIVE =
{
  classes: [
    'Grape::Validations::Validators::MutualExclusionValidator',  # Grape < 3.2
    'Grape::Validations::Validators::MutuallyExclusiveValidator' # Grape >= 3.2
  ].freeze,
  notes: lambda do |groups|
    partners = Hash.new { |hash, key| hash[key] = [] }

    groups.each do |group|
      group.each do |name|
        (group - [name]).each do |other|
          partners[name] << other unless partners[name].include?(other)
        end
      end
    end

    partners.transform_values do |others|
      "Mutually exclusive with #{others.map { |name| "`#{name}`" }.join(', ')}."
    end
  end
}.freeze
CONSTRAINTS =
[MUTUALLY_EXCLUSIVE].freeze

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(route, attributes) ⇒ CrossFieldValidationResolver

attributes are the param names the caller is able to annotate: a route's query params, or a request body's top-level properties. Both readers below share one walk of the route's validations, so a caller that needs both pays for it once.



68
69
70
71
# File 'lib/gitlab/grape_openapi/converters/cross_field_validation_resolver.rb', line 68

def initialize(route, attributes)
  @route = route
  @attributes = attributes
end

Class Method Details

.append_note(description, note) ⇒ Object

Appends a note to an existing description so the two read as separate sentences. Returns the note alone when there is no existing text.



56
57
58
59
60
61
62
# File 'lib/gitlab/grape_openapi/converters/cross_field_validation_resolver.rb', line 56

def self.append_note(description, note)
  text = description.to_s.strip
  return note if text.empty?

  text += '.' unless text.end_with?('.', '!', '?')
  "#{text} #{note}"
end

.notes_for(route, attributes) ⇒ Object

For callers that need notes alone, which is every caller that has no nested scope to skip - see #notes.



49
50
51
# File 'lib/gitlab/grape_openapi/converters/cross_field_validation_resolver.rb', line 49

def self.notes_for(route, attributes)
  new(route, attributes).notes
end

Instance Method Details

#notesObject

Per-param description notes for every cross-field constraint on the route, e.g. { 'files' => 'Mutually exclusive with content.' }. Notes from different constraints on the same param are joined. Returns {} when there are none.



77
78
79
80
81
82
83
84
85
86
# File 'lib/gitlab/grape_openapi/converters/cross_field_validation_resolver.rb', line 77

def notes
  @notes ||= matched_entries.each_with_object({}) do |(constraint, entries), result|
    groups = annotatable_groups(entries)
    next if groups.empty?

    constraint[:notes].call(groups).each do |param, note|
      result[param] = self.class.append_note(result[param], note)
    end
  end
end

#skippedObject

The groups notes could NOT annotate (a member isn't one of attributes).



89
90
91
92
93
# File 'lib/gitlab/grape_openapi/converters/cross_field_validation_resolver.rb', line 89

def skipped
  @skipped ||= matched_entries.flat_map do |_constraint, entries|
    constrained_groups(entries).reject { |group| fully_known?(group) }
  end.uniq
end