Skip to content

Commit

Permalink
Handle parameters from request body. (#38)
Browse files Browse the repository at this point in the history
- simple ~~Arrays~~ objects
- complext objects
  • Loading branch information
LeFnord authored Sep 24, 2023
1 parent 94141e1 commit 815fc38
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 98 deletions.
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ template/Gemfile.lock
grape-starter.md
tmp
api/
.vscode
spec/fixtures/pmm.json
6 changes: 6 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,13 +26,19 @@ Layout/IndentationWidth:
Layout/LineLength:
Max: 120

Lint/MissingSuper:
Enabled: false

Metrics/BlockLength:
Exclude:
- 'spec/**/*'

Metrics/AbcSize:
Max: 20

Metrics/ClassLength:
Max: 120

Metrics/MethodLength:
Max: 20

Expand Down
2 changes: 1 addition & 1 deletion lib/starter/importer/namespace.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ class #{@naming.klass_name} < Grape::API
private

def namespace
naming.version_klass ? "'#{naming.origin}'" : ":#{naming.resource.downcase}"
@namespace ||= naming.version_klass ? "'#{naming.origin}'" : ":#{naming.resource.downcase}"
end

def endpoints
Expand Down
12 changes: 12 additions & 0 deletions lib/starter/importer/nested_params.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# frozen_string_literal: false

module Starter
module Importer
class NestedParams < Parameter
def initialize(name:, definition:)
@name = name
@definition = definition
end
end
end
end
133 changes: 111 additions & 22 deletions lib/starter/importer/parameter.rb
Original file line number Diff line number Diff line change
@@ -1,38 +1,42 @@
# rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
# frozen_string_literal: false

module Starter
module Importer
class Parameter
class Error < StandardError; end

attr_accessor :kind, :name, :definition
attr_accessor :kind, :name, :definition, :nested

def initialize(definition:, components: {})
@nested = []
@kind = validate_parameters(definition:, components:)
prepare_attributes(definition:, components:)
end

def to_s
entry = definition['required'] ? 'requires' : 'optional'
entry << " :#{name}"
entry << ", type: #{definition['schema']['type'].capitalize}"
return serialized_object if nested.present?

doc = documentation
entry << ", #{doc}" if doc
serialized
end

entry
def nested?
@nested.present?
end

private

# initialize helper
#
def validate_parameters(definition:, components:)
return :direct if definition.key?('name')
return :ref if definition.key?('$ref') && components.key?('parameters')
return :body if definition.key?('content')

raise Error, 'no valid combination given'
end

def prepare_attributes(definition:, components:)
def prepare_attributes(definition:, components:) # rubocop:disable Metrics/MethodLength
case kind
when :direct
@name = definition['name']
Expand All @@ -45,26 +49,111 @@ def prepare_attributes(definition:, components:)
if (value = @definition.dig('schema', '$ref').presence)
@definition['schema'] = components.dig(*value.split('/')[2..])
end
when :body
definition['in'] = 'body'
schema = definition['content'].values.first['schema']
if schema.key?('$ref')
path = schema['$ref'].split('/')[2..]

@name, @definition = handle_body(definition:, properties: components.dig(*path))
@name ||= path.last
else
@name, @definition = handle_body(definition:, properties: schema)
@name = nested.map(&:name).join('_') if @name.nil? && nested?
end
end
end

def documentation
tmp = {}
tmp['desc'] = definition['description'] if definition.key?('description')
tmp['in'] = definition['in'] if definition.key?('in')

return nil if tmp.empty?

documentation = 'documentation:'
documentation.tap do |doc|
doc << ' { '
content = tmp.map { |k, v| "#{k}: '#{v}'" }
doc << content.join(', ')
doc << ' }'
def handle_body(definition:, properties:)
if simple_object?(properties:)
name = properties['properties'].keys.first
type = properties.dig('properties', name, 'type') || 'array'
subtype = properties.dig('properties', name, 'items', 'type')
definition['type'] = subtype.nil? ? type : "#{type}[#{subtype}]"

properties.dig('properties', name).except('type').each { |k, v| definition[k] = v }
definition['type'] = 'file' if definition['format'].presence == 'binary'

[name, definition]
elsif object?(definition:) # a nested object -> JSON
definition['type'] = properties['type'].presence || 'JSON'
return [nil, definition] if properties.nil? || properties['properties'].nil?

properties['properties'].each do |nested_name, definition|
definition['required'] = properties['required']&.include?(nested_name) || false
@nested << NestedParams.new(name: nested_name, definition:)
end
[nil, definition]
else # others
[nil, definition.merge(properties)]
end
end

# handle_body helper, check/find/define types
#
def object?(definition:)
definition['content'].keys.first.include?('application/json')
end

def simple_object?(properties:)
properties.key?('properties') &&
properties['properties'].length == 1
end

# to_s helper
#
def serialized_object
definition.tap do |foo|
foo['type'] = foo['type'] == 'object' ? 'JSON' : foo['type']
end

documentation
parent = NestedParams.new(name: name, definition: definition)

entry = "#{parent} do\n"
nested.each { |n| entry << " #{n}\n" }
entry << ' end'
end

def serialized
type = definition['type'] || definition['schema']['type']
type.scan(/\w+/).each { |x| type.match?('JSON') ? type : type.sub!(x, x.capitalize) }

if type == 'Array' && definition.key?('items')
sub = definition.dig('items', 'type').to_s.capitalize
type = "#{type}[#{sub}]"
end

entry = definition['required'] ? 'requires' : 'optional'
entry << " :#{name}"
entry << ", type: #{type}"
doc = documentation
entry << ", #{doc}" if doc

entry
end

def documentation
@documentation ||= begin
tmp = {}
tmp['desc'] = definition['description'] if definition.key?('description')
tmp['in'] = definition['in'] if definition.key?('in')

if definition.key?('format')
tmp['format'] = definition['format']
tmp['type'] = 'File' if definition['format'] == 'binary'
end

documentation = 'documentation:'
documentation.tap do |doc|
doc << ' { '
content = tmp.map { |k, v| "#{k}: '#{v}'" }
doc << content.join(', ')
doc << ' }'
end
end
end
end
end
end

# rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
6 changes: 3 additions & 3 deletions lib/starter/importer/specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ def segmentize(path)
[rest.shift, rest.empty? ? '/' : "/#{rest.join('/')}"]
end

def prepare_verbs(spec)
def prepare_verbs(spec) # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
path_params = nil
spec.each_with_object({}) do |(verb, content), memo|
if verb == 'parameters'
Expand All @@ -56,9 +56,9 @@ def prepare_verbs(spec)
end

memo[verb] = content
next unless content.key?('parameters') || path_params
next unless content.key?('parameters') || content.key?('requestBody') || path_params

parameters = content['parameters'] || path_params
parameters = ((content['parameters'] || path_params || []) + [content['requestBody']]).compact

memo[verb]['parameters'] = parameters.each_with_object({}) do |definition, para|
parameter = Parameter.new(definition:, components:)
Expand Down
Loading

0 comments on commit 815fc38

Please sign in to comment.