|
| 1 | +# frozen_string_literal: true |
| 2 | + |
| 3 | +require 'active_support/core_ext/hash/deep_merge' |
| 4 | + |
| 5 | +module GrapeSwagger |
| 6 | + module DocMethods |
| 7 | + class MoveParams |
| 8 | + class << self |
| 9 | + attr_accessor :definitions |
| 10 | + |
| 11 | + def can_be_moved?(params, http_verb) |
| 12 | + move_methods.include?(http_verb) && includes_body_param?(params) |
| 13 | + end |
| 14 | + |
| 15 | + def to_definition(path, params, route, definitions) |
| 16 | + @definitions = definitions |
| 17 | + unify!(params) |
| 18 | + |
| 19 | + params_to_move = movable_params(params) |
| 20 | + |
| 21 | + return (params + correct_array_param(params_to_move)) if should_correct_array?(params_to_move) |
| 22 | + |
| 23 | + params << parent_definition_of_params(params_to_move, path, route) |
| 24 | + |
| 25 | + params |
| 26 | + end |
| 27 | + |
| 28 | + private |
| 29 | + |
| 30 | + def should_correct_array?(param) |
| 31 | + param.length == 1 && param.first[:in] == 'body' && param.first[:type] == 'array' |
| 32 | + end |
| 33 | + |
| 34 | + def correct_array_param(param) |
| 35 | + param.first[:schema] = { type: param.first.delete(:type), items: param.first.delete(:items) } |
| 36 | + |
| 37 | + param |
| 38 | + end |
| 39 | + |
| 40 | + def parent_definition_of_params(params, path, route) |
| 41 | + definition_name = OperationId.manipulate(parse_model(path)) |
| 42 | + referenced_definition = build_definition(definition_name, params, route.request_method.downcase) |
| 43 | + definition = @definitions[referenced_definition] |
| 44 | + |
| 45 | + move_params_to_new(definition, params) |
| 46 | + |
| 47 | + definition[:description] = route.description if route.try(:description) |
| 48 | + |
| 49 | + build_body_parameter(referenced_definition, definition_name, route.options) |
| 50 | + end |
| 51 | + |
| 52 | + def move_params_to_new(definition, params) |
| 53 | + params, nested_params = params.partition { |x| !x[:name].to_s.include?('[') } |
| 54 | + |
| 55 | + unless params.blank? |
| 56 | + properties, required = build_properties(params) |
| 57 | + add_properties_to_definition(definition, properties, required) |
| 58 | + end |
| 59 | + |
| 60 | + nested_properties = build_nested_properties(nested_params) unless nested_params.blank? |
| 61 | + add_properties_to_definition(definition, nested_properties, []) unless nested_params.blank? |
| 62 | + end |
| 63 | + |
| 64 | + def build_properties(params) |
| 65 | + properties = {} |
| 66 | + required = [] |
| 67 | + |
| 68 | + prepare_nested_types(params) if should_expose_as_array?(params) |
| 69 | + |
| 70 | + params.each do |param| |
| 71 | + name = param[:name].to_sym |
| 72 | + |
| 73 | + properties[name] = if should_expose_as_array?([param]) |
| 74 | + document_as_array(param) |
| 75 | + else |
| 76 | + document_as_property(param) |
| 77 | + end |
| 78 | + |
| 79 | + required << name if deletable?(param) && param[:required] |
| 80 | + end |
| 81 | + |
| 82 | + [properties, required] |
| 83 | + end |
| 84 | + |
| 85 | + def document_as_array(param) |
| 86 | + {}.tap do |property| |
| 87 | + property[:type] = 'array' |
| 88 | + property[:description] = param.delete(:description) unless param[:description].nil? |
| 89 | + property[:items] = document_as_property(param)[:items] |
| 90 | + end |
| 91 | + end |
| 92 | + |
| 93 | + def document_as_property(param) |
| 94 | + property_keys.each_with_object({}) do |x, memo| |
| 95 | + value = param[x] |
| 96 | + value = param[:schema][x] if value.blank? |
| 97 | + next if value.blank? |
| 98 | + |
| 99 | + if x == :type && @definitions[value].present? |
| 100 | + memo['$ref'] = "#/components/schemas/#{value}" |
| 101 | + else |
| 102 | + memo[x] = value |
| 103 | + end |
| 104 | + end |
| 105 | + end |
| 106 | + |
| 107 | + def build_nested_properties(params, properties = {}) |
| 108 | + property = params.bsearch { |x| x[:name].include?('[') }[:name].split('[').first |
| 109 | + |
| 110 | + nested_params, params = params.partition { |x| x[:name].start_with?("#{property}[") } |
| 111 | + prepare_nested_names(property, nested_params) |
| 112 | + |
| 113 | + recursive_call(properties, property, nested_params) unless nested_params.empty? |
| 114 | + build_nested_properties(params, properties) unless params.empty? |
| 115 | + |
| 116 | + properties |
| 117 | + end |
| 118 | + |
| 119 | + def recursive_call(properties, property, nested_params) |
| 120 | + if should_expose_as_array?(nested_params) |
| 121 | + properties[property.to_sym] = array_type |
| 122 | + move_params_to_new(properties[property.to_sym][:items], nested_params) |
| 123 | + else |
| 124 | + properties[property.to_sym] = object_type |
| 125 | + move_params_to_new(properties[property.to_sym], nested_params) |
| 126 | + end |
| 127 | + end |
| 128 | + |
| 129 | + def movable_params(params) |
| 130 | + to_delete = params.each_with_object([]) { |x, memo| memo << x if deletable?(x) } |
| 131 | + delete_from(params, to_delete) |
| 132 | + |
| 133 | + to_delete |
| 134 | + end |
| 135 | + |
| 136 | + def delete_from(params, to_delete) |
| 137 | + to_delete.each { |x| params.delete(x) } |
| 138 | + end |
| 139 | + |
| 140 | + def add_properties_to_definition(definition, properties, required) |
| 141 | + if definition.key?(:items) |
| 142 | + definition[:items][:properties].deep_merge!(properties) |
| 143 | + add_to_required(definition[:items], required) |
| 144 | + else |
| 145 | + definition[:properties].deep_merge!(properties) |
| 146 | + add_to_required(definition, required) |
| 147 | + end |
| 148 | + end |
| 149 | + |
| 150 | + def add_to_required(definition, value) |
| 151 | + return if value.blank? |
| 152 | + |
| 153 | + definition[:required] ||= [] |
| 154 | + definition[:required].push(*value) |
| 155 | + end |
| 156 | + |
| 157 | + def build_body_parameter(reference, name, options) |
| 158 | + {}.tap do |x| |
| 159 | + x[:name] = options[:body_name] || name |
| 160 | + x[:in] = 'body' |
| 161 | + x[:required] = true |
| 162 | + x[:schema] = { '$ref' => "#/components/schemas/#{reference}" } |
| 163 | + end |
| 164 | + end |
| 165 | + |
| 166 | + def build_definition(name, params, verb = nil) |
| 167 | + name = "#{verb}#{name}" if verb |
| 168 | + @definitions[name] = should_expose_as_array?(params) ? array_type : object_type |
| 169 | + |
| 170 | + name |
| 171 | + end |
| 172 | + |
| 173 | + def array_type |
| 174 | + { type: 'array', items: { type: 'object', properties: {} } } |
| 175 | + end |
| 176 | + |
| 177 | + def object_type |
| 178 | + { type: 'object', properties: {} } |
| 179 | + end |
| 180 | + |
| 181 | + def prepare_nested_types(params) |
| 182 | + params.each do |param| |
| 183 | + next unless param[:items] |
| 184 | + |
| 185 | + param[:schema][:type] = if param[:items][:type] == 'array' |
| 186 | + 'string' |
| 187 | + elsif param[:items].key?('$ref') |
| 188 | + param[:schema][:type] = 'object' |
| 189 | + else |
| 190 | + param[:items][:type] |
| 191 | + end |
| 192 | + param[:schema][:format] = param[:items][:format] if param[:items][:format] |
| 193 | + param.delete(:items) if param[:schema][:type] != 'object' |
| 194 | + end |
| 195 | + end |
| 196 | + |
| 197 | + def prepare_nested_names(property, params) |
| 198 | + params.each { |x| x[:name] = x[:name].sub(property, '').sub('[', '').sub(']', '') } |
| 199 | + end |
| 200 | + |
| 201 | + def unify!(params) |
| 202 | + params.each { |x| x[:in] = x.delete(:param_type) if x[:param_type] } |
| 203 | + params.each { |x| x[:in] = 'body' if x[:in] == 'formData' } if includes_body_param?(params) |
| 204 | + end |
| 205 | + |
| 206 | + def parse_model(ref) |
| 207 | + parts = ref.split('/') |
| 208 | + parts.last.include?('{') ? parts[0..-2].join('/') : parts[0..-1].join('/') |
| 209 | + end |
| 210 | + |
| 211 | + def property_keys |
| 212 | + %i[type format description minimum maximum items enum default] |
| 213 | + end |
| 214 | + |
| 215 | + def deletable?(param) |
| 216 | + param[:in] == 'body' |
| 217 | + end |
| 218 | + |
| 219 | + def move_methods |
| 220 | + [:post, :put, :patch, 'POST', 'PUT', 'PATCH'] |
| 221 | + end |
| 222 | + |
| 223 | + def includes_body_param?(params) |
| 224 | + params.map { |x| return true if x[:in] == 'body' || x[:param_type] == 'body' } |
| 225 | + false |
| 226 | + end |
| 227 | + |
| 228 | + def should_expose_as_array?(params) |
| 229 | + should_exposed_as(params) == 'array' |
| 230 | + end |
| 231 | + |
| 232 | + def should_exposed_as(params) |
| 233 | + params.map { |x| return 'object' if x[:schema][:type] && x[:schema][:type] != 'array' } |
| 234 | + 'array' |
| 235 | + end |
| 236 | + end |
| 237 | + end |
| 238 | + end |
| 239 | +end |
0 commit comments