Skip to content

Commit d16a5ac

Browse files
Update script to define entire module body in a single method
1 parent d1dde30 commit d16a5ac

1 file changed

Lines changed: 138 additions & 191 deletions

File tree

bin/generate_sdk_from_openapi_spec.rb

Lines changed: 138 additions & 191 deletions
Original file line numberDiff line numberDiff line change
@@ -13,221 +13,133 @@
1313
WRAP_LINE_LENGTH = 80
1414
TOP_LEVEL_RESPONSE_KEYS = %w[status message data meta].freeze
1515

16-
def api_methods_by_api_module_name
17-
@api_methods_by_api_module_name ||=
18-
document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module|
19-
%w[get post put patch delete].each do |http_method|
20-
operation = path_item.operation(http_method)
21-
next if !operation
22-
23-
api_module_name = operation.tags.first.remove(' ')
24-
next if !api_module_name
25-
26-
paths_by_api_module[api_module_name] ||= []
27-
paths_by_api_module[api_module_name] << { path:, http_method:, operation: }
28-
end
29-
end
30-
end
31-
32-
def update_required_files(module_names)
33-
file_lines = File.readlines(LIB_FILE)
34-
35-
comment_start_index = file_lines.find_index { |line| line.include?('# API Modules') }
36-
if !comment_start_index
37-
puts "Could not find '# API Modules' comment in #{LIB_FILE}"
38-
return
39-
end
40-
41-
insertion_start = comment_start_index + 1
42-
insertion_start += 1 while insertion_start < file_lines.size && file_lines[insertion_start].start_with?('#')
43-
44-
insertion_end = insertion_start
45-
insertion_end += 1 while insertion_end < file_lines.size && !file_lines[insertion_end].strip.empty?
46-
47-
file_lines[insertion_start...insertion_end] =
48-
module_names.map { |module_name| "require 'paystack_gateway/#{module_name.underscore}'\n" }.sort.join
49-
50-
File.write(LIB_FILE, file_lines.join)
51-
52-
puts 'Updated paystack_gateway.rb with new API Modules requires.'
53-
end
54-
55-
def api_module_content(api_module_name, api_methods)
16+
def api_module_content(api_module_name, api_methods) # rubocop:disable Metrics/AbcSize,Metrics/MethodLength,Metrics/CyclomaticComplexity,Metrics/PerceivedComplexity
17+
module_info = tags_by_name[api_module_name]
5618
<<~RUBY
5719
# frozen_string_literal: true
5820
5921
module PaystackGateway
60-
#{api_module_docstring(api_module_name)}
22+
# https://paystack.com/docs/api/#{api_module_name.parameterize}
23+
#
24+
# #{module_info['x-product-name'].squish || api_module_name.humanize}
25+
# #{module_info['description'].squish}
6126
module #{api_module_name}
6227
include PaystackGateway::RequestModule
6328
64-
#{api_methods.map { |info| api_method_composition(api_module_name, info) }.join("\n").chomp}
29+
#{
30+
api_methods.map do |api_method_info| # rubocop:disable Metrics/BlockLength
31+
api_method_info => { path:, http_method:, operation: }
32+
33+
api_method_name = api_method_name(api_module_name, operation)
34+
<<~RUBY.chomp
35+
#{INDENT * 2}# Successful response from calling ##{api_method_name}.
36+
#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response#{
37+
if (data_keys = api_response_data_keys(operation)).any?
38+
if data_keys.length > 3
39+
"\n#{INDENT * 3}delegate #{data_keys.map { |key| ":#{key}," }.join("\n#{INDENT * 3} #{' ' * 'delegate'.length}")} to: :data\n#{INDENT * 2}end"
40+
else
41+
"\n#{INDENT * 3}delegate #{data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data\n#{INDENT * 2}end"
42+
end
43+
else
44+
'; end'
45+
end
46+
}
47+
48+
#{INDENT * 2}# Error response from ##{api_method_name}.
49+
#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end
50+
51+
#{INDENT * 2}# https://paystack.com/docs/api/#{operation.tags.first.parameterize}/##{api_method_name}
52+
#{INDENT * 2}# #{operation.summary}: #{http_method.upcase} #{path}#{
53+
operation.description ? "\n#{INDENT * 2}# #{operation.description}" : ''
54+
}
55+
#{INDENT * 2}##{
56+
docstring = ''
57+
api_method_parameters(operation).each do |param|
58+
docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]"
59+
docstring += ' (required)' if param[:required]
60+
61+
if (description = param[:description]&.squish)
62+
docstring += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ")
63+
end
64+
65+
next if !(object_properties = param[:object_properties])
66+
67+
object_properties.each do |props|
68+
docstring += "\n#{INDENT * 2}##{INDENT * 1} @option #{param[:name]} [#{props[:type]}] :#{props[:name]}"
69+
docstring += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 5}").to_s
70+
end
71+
end
72+
73+
docstring
74+
}
75+
#{INDENT * 2}#
76+
#{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response
77+
#{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails
78+
#{INDENT * 2}api_method def self.#{api_method_name}#{
79+
if (method_args = api_method_args(operation)).any?
80+
if method_args.length > 5
81+
"(#{method_args.map { |arg| "\n#{INDENT * 3}#{arg}" }.join(',')}\n#{INDENT * 2})"
82+
else
83+
"(#{method_args.join(', ')})"
84+
end
85+
end
86+
}
87+
#{INDENT * 2} use_connection do |connection|
88+
#{INDENT * 2} connection.#{http_method}(
89+
#{INDENT * 2} #{
90+
if path == (interpolated = path.gsub(/{([^}]+)}/, "\#{\\1}"))
91+
"'#{path}'"
92+
else
93+
"\"#{interpolated}\""
94+
end
95+
},#{
96+
if (request_args = api_request_args(operation)).any?
97+
if request_args.length > 5
98+
"\n#{INDENT * 5}{#{request_args.map { |arg| "\n#{INDENT * 6}#{arg}," }.join}\n#{INDENT * 5}}.compact,"
99+
else
100+
"\n#{INDENT * 5}{ #{request_args.join(', ')} }.compact,"
101+
end
102+
end
103+
}
104+
#{INDENT * 2} )
105+
#{INDENT * 2} end
106+
#{INDENT * 2}end
107+
108+
RUBY
109+
end.join("\n").chomp
110+
}
65111
end
66112
end
67113
RUBY
68114
end
69115

70-
def api_module_docstring(api_module_name)
71-
module_info = tags_by_name[api_module_name]
72-
return if !module_info
73-
74-
docstring = "# https://paystack.com/docs/api/#{api_module_name.parameterize}"
75-
docstring += "\n#{INDENT * 1}#"
76-
docstring += "\n#{INDENT * 1}# #{module_info['x-product-name'].squish || api_module_name.humanize}"
77-
78-
docstring += "\n#{INDENT * 1}# #{module_info['description'].squish}" if module_info['description']
79-
docstring
80-
end
81-
82-
def api_method_composition(api_module_name, api_method_info)
83-
api_method_info => { path:, http_method:, operation: }
84-
85-
api_method_name = api_method_name(api_module_name, operation)
86-
<<~RUBY.chomp
87-
#{api_method_response_class_content(api_method_name, operation)}
88-
89-
#{api_method_error_class_content(api_method_name)}
90-
91-
#{api_method_content(api_method_name, operation, http_method, path)}
92-
RUBY
93-
end
94-
95-
def api_method_response_class_content(api_method_name, operation)
96-
definition = "#{INDENT * 2}# Successful response from calling ##{api_method_name}.\n" \
97-
"#{INDENT * 2}class #{"#{api_method_name}_response".camelize} < PaystackGateway::Response"
98-
99-
if (delegate_content = api_method_response_class_delegate_content(operation))
100-
"#{definition}#{delegate_content}\n#{INDENT * 2}end"
101-
else
102-
"#{definition}; end"
103-
end
104-
end
105-
106-
def api_method_response_class_delegate_content(operation)
116+
def api_response_data_keys(operation)
107117
responses = operation.responses.response
108118
success_response = responses[responses.keys.find { _1.match?(/\A2..\z/) }]
109-
return if !success_response
119+
return [] if !success_response
110120

111121
required_data_keys = success_response.content['application/json'].schema.properties['data']&.required || []
112-
113-
required_data_keys -= TOP_LEVEL_RESPONSE_KEYS
114-
return if required_data_keys.none?
115-
116-
if required_data_keys.length > 3
117-
definition = "\n#{INDENT * 3}delegate :#{required_data_keys.shift},"
118-
119-
while (line_key = required_data_keys.shift).present?
120-
definition += "\n#{INDENT * 3}#{' ' * 'delegate'.length} :#{line_key},"
121-
end
122-
123-
"#{definition} to: :data"
124-
else
125-
"\n#{INDENT * 3}delegate #{required_data_keys.map { |key| ":#{key}" }.join(', ')}, to: :data"
126-
end
127-
end
128-
129-
def api_method_error_class_content(api_method_name)
130-
"#{INDENT * 2}# Error response from ##{api_method_name}.\n" \
131-
"#{INDENT * 2}class #{"#{api_method_name}_error".camelize} < ApiError; end"
122+
required_data_keys - TOP_LEVEL_RESPONSE_KEYS
132123
end
133124

134-
def api_method_content(api_method_name, operation, http_method, path)
135-
<<-RUBY
136-
#{api_method_definition_header_docstring(api_method_name, operation, http_method, path)}
137-
#{api_method_definition_params_docstring(operation)}
138-
#{api_method_definition_response_docstring(api_method_name)}
139-
#{api_method_definition_name_and_parameters(api_method_name, operation)}
140-
use_connection do |connection|
141-
connection.#{http_method}(
142-
#{api_method_definition_path(path)},#{api_method_definition_request_params(operation)}
143-
)
144-
end
145-
end
146-
RUBY
147-
end
148-
149-
def api_method_definition_header_docstring(api_method_name, operation, http_method, path)
150-
docstring = "# https://paystack.com/docs/api/#{operation.tags.first.parameterize}/##{api_method_name}"
151-
docstring += "\n#{INDENT * 2}# #{operation.summary}: #{http_method.upcase} #{path}"
152-
docstring += "\n#{INDENT * 2}# #{operation.description}" if operation.description.present?
153-
docstring
154-
end
155-
156-
def api_method_definition_params_docstring(operation)
157-
docstring = '#'
158-
159-
api_method_parameters(operation).each do |param|
160-
docstring += "\n#{INDENT * 2}# @param #{param[:name]} [#{param[:type]}]"
161-
docstring += ' (required)' if param[:required]
162-
163-
if (description = param[:description]&.squish)
164-
docstring += wrapped_text(description, "\n#{INDENT * 2}##{INDENT * 3} ")
165-
end
166-
167-
next if !(object_properties = param[:object_properties])
168-
169-
object_properties.each do |props|
170-
docstring += "\n#{INDENT * 2}##{INDENT * 1} @option #{param[:name]} [#{props[:type]}] :#{props[:name]}"
171-
docstring += wrapped_text(props[:description], "\n#{INDENT * 2}##{INDENT * 5}")
172-
end
173-
end
174-
175-
docstring
176-
end
177-
178-
def api_method_definition_response_docstring(api_method_name)
179-
docstring = '#'
180-
docstring += "\n#{INDENT * 2}# @return [#{"#{api_method_name}_response".camelize}] successful response"
181-
docstring + "\n#{INDENT * 2}# @raise [#{"#{api_method_name}_error".camelize}] if the request fails"
182-
end
183-
184-
def api_method_definition_name_and_parameters(api_method_name, operation)
185-
method_args = api_method_parameters(operation).map do |param|
125+
def api_method_args(operation)
126+
api_method_parameters(operation).map do |param|
186127
name = param[:name].underscore
187128
param[:required] ? "#{name}:" : "#{name}: nil"
188129
end
189-
190-
definition = "api_method def self.#{api_method_name}"
191-
return definition if method_args.none?
192-
193-
definition += '('
194-
195-
if method_args.length > 5
196-
while (line_arg = method_args.shift).present?
197-
definition += "\n#{INDENT * 3}#{line_arg}"
198-
definition += ',' if method_args.any?
199-
end
200-
definition + "\n#{INDENT * 2})"
201-
else
202-
definition + "#{method_args.join(', ')})"
203-
end
204-
end
205-
206-
def api_method_definition_path(path)
207-
# "/transaction/verify/{reference}" -> "/transaction/verify/#{reference}"
208-
interpolated_path = path.gsub(/{([^}]+)}/, "\#{\\1}")
209-
210-
interpolated_path == path ? "'#{interpolated_path}'" : "\"#{interpolated_path}\""
211130
end
212131

213-
def api_method_definition_request_params(operation)
214-
params = api_method_parameters(operation)
215-
.reject { |param| param[:in] == 'path' }
216-
.map { |param| param[:name] }
217-
return if params.none?
218-
219-
definition = "\n#{INDENT * 5}{"
132+
def api_request_args(operation)
133+
api_method_parameters(operation)
134+
.filter_map do |param|
135+
next if param[:in] == 'path'
220136

221-
if params.length > 5
222-
while (line_param = params.shift).present?
223-
definition += "\n#{INDENT * 6}#{line_param}:" \
224-
"#{line_param == line_param.underscore ? nil : " #{line_param.underscore}"},"
137+
if param[:name] == param[:name].underscore
138+
"#{param[:name]}:"
139+
else
140+
"#{param[:name]}: #{param[:name].underscore}"
141+
end
225142
end
226-
227-
definition + "\n#{INDENT * 5}}.compact,"
228-
else
229-
"#{definition} #{params.map { |param| "#{param}:" }.join(', ')} }.compact,"
230-
end
231143
end
232144

233145
def api_method_parameters(operation)
@@ -365,6 +277,22 @@ def document
365277
@document ||= OpenAPIParser.parse(YAML.load_file(OPENAPI_SPEC), strict_reference_validation: true)
366278
end
367279

280+
def api_methods_by_api_module_name
281+
@api_methods_by_api_module_name ||=
282+
document.paths.path.each_with_object({}) do |(path, path_item), paths_by_api_module|
283+
%w[get post put patch delete].each do |http_method|
284+
operation = path_item.operation(http_method)
285+
next if !operation
286+
287+
api_module_name = operation.tags.first.remove(' ')
288+
next if !api_module_name
289+
290+
paths_by_api_module[api_module_name] ||= []
291+
paths_by_api_module[api_module_name] << { path:, http_method:, operation: }
292+
end
293+
end
294+
end
295+
368296
module_names = api_methods_by_api_module_name.map do |api_module_name, api_methods|
369297
if existing_api_modules.include?(api_module_name)
370298
puts "Updating existing module: #{api_module_name}"
@@ -381,4 +309,23 @@ def document
381309
api_module_name
382310
end
383311

384-
update_required_files(module_names)
312+
lib_file_lines = File.readlines(LIB_FILE)
313+
314+
comment_start_index = lib_file_lines.find_index { |line| line.include?('# API Modules') }
315+
if !comment_start_index
316+
puts "Could not find '# API Modules' comment in #{LIB_FILE}"
317+
exit 1
318+
end
319+
320+
insertion_start = comment_start_index + 1
321+
insertion_start += 1 while insertion_start < lib_file_lines.size && lib_file_lines[insertion_start].start_with?('#')
322+
323+
insertion_end = insertion_start
324+
insertion_end += 1 while insertion_end < lib_file_lines.size && !lib_file_lines[insertion_end].strip.empty?
325+
326+
lib_file_lines[insertion_start...insertion_end] =
327+
module_names.map { |module_name| "require 'paystack_gateway/#{module_name.underscore}'\n" }.sort.join
328+
329+
File.write(LIB_FILE, lib_file_lines.join)
330+
331+
puts 'Updated paystack_gateway.rb with new API Modules requires.'

0 commit comments

Comments
 (0)