Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Handle parameters from request body. #38

Merged
merged 1 commit into from
Sep 24, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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