taimour_openapi_sdk_generator 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/openapi-sdk-generator +57 -0
- data/lib/openapi_sdk_generator/generator.rb +36 -0
- data/lib/openapi_sdk_generator/generators/javascript_generator.rb +107 -0
- data/lib/openapi_sdk_generator/generators/ruby_generator.rb +148 -0
- data/lib/openapi_sdk_generator/parser.rb +158 -0
- data/lib/openapi_sdk_generator/templates/javascript_client.erb +102 -0
- data/lib/openapi_sdk_generator/templates/ruby_client.erb +87 -0
- data/lib/openapi_sdk_generator/templates/ruby_model.erb +29 -0
- data/lib/openapi_sdk_generator.rb +58 -0
- metadata +92 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: edb42df3a6a1c4802d39ed957bbb54daa2d3d2352524a06486412477336db50d
|
|
4
|
+
data.tar.gz: d98bce34a6c069bd33bc0d1e1e127122df935077e8c278bf3fefa783bb6c921f
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 4f49be3da129f05378d4946a921b0fa7e164473c225b2418b3dfb04e696bdf9288f50c1534b71dd4c172b2ea650e963edebfa9c98c1a8136c4c7a167162e0634
|
|
7
|
+
data.tar.gz: 488c788f5e003b49247311b16a809f828ea7da34b261ea9cc0b0932b839fd55680d6a7cccbe5bf819c963761e72afa065c0e738ac5ed49cbac98f3db15983a42
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require 'optparse'
|
|
4
|
+
require_relative '../lib/openapi_sdk_generator'
|
|
5
|
+
|
|
6
|
+
options = {}
|
|
7
|
+
|
|
8
|
+
parser = OptionParser.new do |opts|
|
|
9
|
+
opts.banner = "Usage: openapi-sdk-generator [options]"
|
|
10
|
+
opts.separator ""
|
|
11
|
+
opts.separator "Generate SDK from OpenAPI specification"
|
|
12
|
+
opts.separator ""
|
|
13
|
+
|
|
14
|
+
opts.on("-i", "--input FILE", "OpenAPI specification file (JSON or YAML)") do |v|
|
|
15
|
+
options[:input] = v
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
opts.on("-o", "--output DIR", "Output directory for generated SDK") do |v|
|
|
19
|
+
options[:output] = v
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
opts.on("-l", "--language LANG", "Target language (ruby, javascript)") do |v|
|
|
23
|
+
options[:language] = v
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
opts.on("-h", "--help", "Show this help message") do
|
|
27
|
+
puts opts
|
|
28
|
+
exit
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
opts.on("-v", "--version", "Show version") do
|
|
32
|
+
puts "OpenAPI SDK Generator v0.1.0"
|
|
33
|
+
exit
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
begin
|
|
38
|
+
parser.parse!
|
|
39
|
+
|
|
40
|
+
if options.empty?
|
|
41
|
+
puts parser
|
|
42
|
+
exit
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
OpenapiSdkGenerator::CLI.new(options).run
|
|
46
|
+
rescue OptionParser::InvalidOption, OptionParser::MissingArgument => e
|
|
47
|
+
puts "Error: #{e.message}"
|
|
48
|
+
puts parser
|
|
49
|
+
exit 1
|
|
50
|
+
rescue OpenapiSdkGenerator::Error => e
|
|
51
|
+
puts "Error: #{e.message}"
|
|
52
|
+
exit 1
|
|
53
|
+
rescue => e
|
|
54
|
+
puts "Unexpected error: #{e.message}"
|
|
55
|
+
puts e.backtrace.first(5)
|
|
56
|
+
exit 1
|
|
57
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module OpenapiSdkGenerator
|
|
2
|
+
class Generator
|
|
3
|
+
attr_reader :parser
|
|
4
|
+
|
|
5
|
+
def initialize(parser)
|
|
6
|
+
@parser = parser
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def generate
|
|
10
|
+
raise NotImplementedError, "Subclasses must implement #generate"
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def write_to_directory(output_dir)
|
|
14
|
+
raise NotImplementedError, "Subclasses must implement #write_to_directory"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
protected
|
|
18
|
+
|
|
19
|
+
def render_template(template_name, binding_context)
|
|
20
|
+
template_path = File.join(__dir__, 'templates', template_name)
|
|
21
|
+
template = File.read(template_path)
|
|
22
|
+
ERB.new(template, trim_mode: '-').result(binding_context)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def sanitize_name(name)
|
|
26
|
+
puts "DEBUG param: #{p.inspect} (#{p.class})"
|
|
27
|
+
|
|
28
|
+
# Convert to snake_case and remove special characters
|
|
29
|
+
name.gsub(/[^a-zA-Z0-9_]/, '_').gsub(/_{2,}/, '_').downcase
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def camelize(string)
|
|
33
|
+
string.split('_').map(&:capitalize).join
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
module OpenapiSdkGenerator
|
|
2
|
+
module Generators
|
|
3
|
+
class JavascriptGenerator < Generator
|
|
4
|
+
def generate
|
|
5
|
+
{
|
|
6
|
+
client: generate_client
|
|
7
|
+
}
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def write_to_directory(output_dir)
|
|
11
|
+
FileUtils.mkdir_p(output_dir)
|
|
12
|
+
|
|
13
|
+
# Write client file
|
|
14
|
+
client_content = generate_client
|
|
15
|
+
File.write(File.join(output_dir, 'client.js'), client_content)
|
|
16
|
+
|
|
17
|
+
# Write package.json
|
|
18
|
+
package_json = generate_package_json
|
|
19
|
+
File.write(File.join(output_dir, 'package.json'), package_json)
|
|
20
|
+
|
|
21
|
+
# Write README
|
|
22
|
+
readme_content = generate_readme
|
|
23
|
+
File.write(File.join(output_dir, 'README.md'), readme_content)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def generate_client
|
|
30
|
+
render_template('javascript_client.erb', binding)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def format_js_params(endpoint)
|
|
34
|
+
return "" unless endpoint[:parameters]
|
|
35
|
+
|
|
36
|
+
endpoint[:parameters].map { |p| sanitize_name(p[:name]) }.join(", ")
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def generate_client
|
|
40
|
+
render_template('javascript_client.erb', binding)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def generate_package_json
|
|
44
|
+
{
|
|
45
|
+
name: sanitize_name(parser.api_title).tr('_', '-'),
|
|
46
|
+
version: parser.api_version,
|
|
47
|
+
description: parser.api_description,
|
|
48
|
+
main: "client.js",
|
|
49
|
+
scripts: {
|
|
50
|
+
test: "echo \"Error: no test specified\" && exit 1"
|
|
51
|
+
},
|
|
52
|
+
keywords: ["api", "client", "sdk"],
|
|
53
|
+
author: "",
|
|
54
|
+
license: "MIT"
|
|
55
|
+
}.to_json
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def generate_readme
|
|
59
|
+
<<~README
|
|
60
|
+
# #{parser.api_title}
|
|
61
|
+
|
|
62
|
+
#{parser.api_description}
|
|
63
|
+
|
|
64
|
+
Version: #{parser.api_version}
|
|
65
|
+
|
|
66
|
+
## Installation
|
|
67
|
+
```bash
|
|
68
|
+
|
|
69
|
+
npm install
|
|
70
|
+
|
|
71
|
+
## Usage
|
|
72
|
+
```javascript
|
|
73
|
+
|
|
74
|
+
const APIClient = require('./client');
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
// Example usage
|
|
78
|
+
#{parser.endpoints.first ? "// #{parser.endpoints.first[:summary]}" : '// Make API calls'}
|
|
79
|
+
#{parser.endpoints.first ? "// const response = await client.#{js_method_name(parser.endpoints.first)}(...);" : ''}
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
## Available Methods
|
|
83
|
+
|
|
84
|
+
#{parser.endpoints.map { |e| "- `#{js_method_name(e)}()` - #{e[:summary]}" }.join("\n")}
|
|
85
|
+
README
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def js_method_name(endpoint)
|
|
89
|
+
op = endpoint[:operation_id] || "#{endpoint[:method]}_#{endpoint[:path]}"
|
|
90
|
+
parts = sanitize_name(op).split('_')
|
|
91
|
+
parts.first + parts[1..].map(&:capitalize).join
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def js_type(openapi_type)
|
|
96
|
+
case openapi_type
|
|
97
|
+
when 'integer', 'number' then 'number'
|
|
98
|
+
when 'string' then 'string'
|
|
99
|
+
when 'boolean' then 'boolean'
|
|
100
|
+
when 'array' then 'array'
|
|
101
|
+
when 'object' then 'object'
|
|
102
|
+
else 'any'
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
module OpenapiSdkGenerator
|
|
2
|
+
module Generators
|
|
3
|
+
class RubyGenerator < Generator
|
|
4
|
+
def generate
|
|
5
|
+
{
|
|
6
|
+
client: generate_client,
|
|
7
|
+
models: generate_models
|
|
8
|
+
}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def write_to_directory(output_dir)
|
|
12
|
+
FileUtils.mkdir_p(output_dir)
|
|
13
|
+
FileUtils.mkdir_p(File.join(output_dir, 'models'))
|
|
14
|
+
|
|
15
|
+
# Write client file
|
|
16
|
+
client_content = generate_client
|
|
17
|
+
File.write(File.join(output_dir, 'client.rb'), client_content)
|
|
18
|
+
|
|
19
|
+
# Write model files
|
|
20
|
+
parser.models.each do |name, model|
|
|
21
|
+
model_content = generate_model(model)
|
|
22
|
+
filename = "#{sanitize_name(name)}.rb"
|
|
23
|
+
File.write(File.join(output_dir, 'models', filename), model_content)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Write README
|
|
27
|
+
readme_content = generate_readme
|
|
28
|
+
File.write(File.join(output_dir, 'README.md'), readme_content)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def generate_client
|
|
34
|
+
render_template('ruby_client.erb', binding)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def generate_models
|
|
38
|
+
parser.models.map do |name, model|
|
|
39
|
+
generate_model(model)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def generate_model(model)
|
|
44
|
+
@current_model = model
|
|
45
|
+
render_template('ruby_model.erb', binding)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def generate_readme
|
|
49
|
+
<<~README
|
|
50
|
+
# #{parser.api_title}
|
|
51
|
+
|
|
52
|
+
#{parser.api_description}
|
|
53
|
+
|
|
54
|
+
Version: #{parser.api_version}
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
Add this to your application's Gemfile:
|
|
59
|
+
```ruby
|
|
60
|
+
gem 'your_gem_name'
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
## Usage
|
|
64
|
+
```ruby
|
|
65
|
+
require_relative 'client'
|
|
66
|
+
|
|
67
|
+
client = APIClient.new('#{parser.base_url}')
|
|
68
|
+
|
|
69
|
+
# Example usage
|
|
70
|
+
#{parser.endpoints.first ? "# #{parser.endpoints.first[:summary]}" : '# Make API calls'}
|
|
71
|
+
#{parser.endpoints.first ? "# response = client.#{method_name(parser.endpoints.first)}(...)" : ''}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Available Methods
|
|
75
|
+
|
|
76
|
+
#{parser.endpoints.map { |e| "- `#{method_name(e)}` - #{e[:summary]}" }.join("\n")}
|
|
77
|
+
README
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def method_name(endpoint)
|
|
81
|
+
sanitize_name(endpoint[:operation_id])
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def ruby_type(openapi_type)
|
|
85
|
+
case openapi_type
|
|
86
|
+
when 'integer' then 'Integer'
|
|
87
|
+
when 'number' then 'Float'
|
|
88
|
+
when 'string' then 'String'
|
|
89
|
+
when 'boolean' then 'Boolean'
|
|
90
|
+
when 'array' then 'Array'
|
|
91
|
+
when 'object' then 'Hash'
|
|
92
|
+
else 'Object'
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
# Converts parameters into Ruby method signature
|
|
100
|
+
def format_method_params(params)
|
|
101
|
+
return "" unless params.is_a?(Array) && !params.empty?
|
|
102
|
+
|
|
103
|
+
params.map do |p|
|
|
104
|
+
next unless p.is_a?(Hash)
|
|
105
|
+
|
|
106
|
+
name = sanitize_name(p[:name].to_s)
|
|
107
|
+
required = p[:required] ? "" : " = nil"
|
|
108
|
+
"#{name}#{required}"
|
|
109
|
+
end.compact.join(", ")
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
# Converts "/pets/{petId}" → "/pets/#{pet_id}"
|
|
113
|
+
def format_url_path(path, params = [])
|
|
114
|
+
return path unless params.is_a?(Array) && !params.empty?
|
|
115
|
+
|
|
116
|
+
params
|
|
117
|
+
.select { |p| p[:location] == "path" } # FIXED HERE
|
|
118
|
+
.reduce(path) do |memo, p|
|
|
119
|
+
ruby_name = sanitize_name(p[:name].to_s)
|
|
120
|
+
memo.gsub("{#{p[:name]}}", "\#{#{ruby_name}}")
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
# Converts OpenAPI type to Ruby class (used by templates)
|
|
125
|
+
def format_ruby_type(schema)
|
|
126
|
+
return "Object" if schema.nil?
|
|
127
|
+
|
|
128
|
+
case schema[:type]
|
|
129
|
+
when "integer" then "Integer"
|
|
130
|
+
when "number" then "Float"
|
|
131
|
+
when "string" then "String"
|
|
132
|
+
when "boolean" then "Boolean"
|
|
133
|
+
when "array" then "Array"
|
|
134
|
+
when "object" then "Hash"
|
|
135
|
+
else
|
|
136
|
+
if schema.is_a?(Hash) && schema["$ref"]
|
|
137
|
+
sanitize_name(schema["$ref"].split('/').last)
|
|
138
|
+
else
|
|
139
|
+
"Object"
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
module OpenapiSdkGenerator
|
|
2
|
+
class Parser
|
|
3
|
+
attr_reader :spec, :api_info, :base_url, :endpoints, :models
|
|
4
|
+
|
|
5
|
+
def initialize(file_path)
|
|
6
|
+
@file_path = file_path
|
|
7
|
+
@spec = load_spec
|
|
8
|
+
@endpoints = []
|
|
9
|
+
@models = {}
|
|
10
|
+
@api_info = {}
|
|
11
|
+
parse_spec
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def api_title
|
|
15
|
+
@api_info[:title] || 'Generated API Client'
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def api_version
|
|
19
|
+
@api_info[:version] || '1.0.0'
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def api_description
|
|
23
|
+
@api_info[:description] || 'API client generated from OpenAPI specification'
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
private
|
|
27
|
+
|
|
28
|
+
def load_spec
|
|
29
|
+
content = File.read(@file_path)
|
|
30
|
+
if @file_path.end_with?('.json')
|
|
31
|
+
JSON.parse(content)
|
|
32
|
+
elsif @file_path.end_with?('.yaml', '.yml')
|
|
33
|
+
YAML.load(content)
|
|
34
|
+
else
|
|
35
|
+
raise Error, "Unsupported file format. Use .json, .yaml, or .yml"
|
|
36
|
+
end
|
|
37
|
+
rescue => e
|
|
38
|
+
raise Error, "Failed to parse OpenAPI spec: #{e.message}"
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def parse_spec
|
|
42
|
+
parse_info
|
|
43
|
+
parse_servers
|
|
44
|
+
parse_paths
|
|
45
|
+
parse_schemas
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def parse_info
|
|
49
|
+
info = @spec['info'] || {}
|
|
50
|
+
@api_info = {
|
|
51
|
+
title: info['title'],
|
|
52
|
+
version: info['version'],
|
|
53
|
+
description: info['description']
|
|
54
|
+
}
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def parse_servers
|
|
58
|
+
servers = @spec['servers'] || []
|
|
59
|
+
@base_url = servers.first&.dig('url') || 'https://api.example.com'
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def parse_paths
|
|
63
|
+
paths = @spec['paths'] || {}
|
|
64
|
+
|
|
65
|
+
paths.each do |path, methods|
|
|
66
|
+
methods.each do |method, details|
|
|
67
|
+
next if method.start_with?('$') # Skip special keys like $ref
|
|
68
|
+
|
|
69
|
+
@endpoints << {
|
|
70
|
+
path: path,
|
|
71
|
+
method: method.upcase,
|
|
72
|
+
operation_id: details['operationId'] || generate_operation_id(method, path),
|
|
73
|
+
summary: details['summary'],
|
|
74
|
+
description: details['description'],
|
|
75
|
+
parameters: parse_parameters(details['parameters']),
|
|
76
|
+
request_body: parse_request_body(details['requestBody']),
|
|
77
|
+
responses: parse_responses(details['responses'])
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def parse_parameters(parameters)
|
|
84
|
+
return [] unless parameters
|
|
85
|
+
|
|
86
|
+
parameters.map do |param|
|
|
87
|
+
{
|
|
88
|
+
name: param['name'],
|
|
89
|
+
location: param['in'],
|
|
90
|
+
required: param['required'] || false,
|
|
91
|
+
type: param['schema']&.dig('type') || 'string',
|
|
92
|
+
description: param['description']
|
|
93
|
+
}
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def parse_request_body(request_body)
|
|
98
|
+
return nil unless request_body
|
|
99
|
+
|
|
100
|
+
content = request_body['content'] || {}
|
|
101
|
+
json_content = content['application/json'] || {}
|
|
102
|
+
|
|
103
|
+
{
|
|
104
|
+
required: request_body['required'] || false,
|
|
105
|
+
schema: json_content['schema']
|
|
106
|
+
}
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def parse_responses(responses)
|
|
110
|
+
return {} unless responses
|
|
111
|
+
|
|
112
|
+
responses.transform_values do |response|
|
|
113
|
+
{
|
|
114
|
+
description: response['description'],
|
|
115
|
+
content: response['content']
|
|
116
|
+
}
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_schemas
|
|
121
|
+
components = @spec['components'] || {}
|
|
122
|
+
schemas = components['schemas'] || {}
|
|
123
|
+
|
|
124
|
+
schemas.each do |name, schema|
|
|
125
|
+
@models[name] = {
|
|
126
|
+
name: name,
|
|
127
|
+
type: schema['type'],
|
|
128
|
+
properties: parse_properties(schema['properties']),
|
|
129
|
+
required: schema['required'] || []
|
|
130
|
+
}
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def parse_properties(properties)
|
|
135
|
+
return {} unless properties
|
|
136
|
+
|
|
137
|
+
properties.transform_values do |prop|
|
|
138
|
+
{
|
|
139
|
+
type: prop['type'],
|
|
140
|
+
format: prop['format'],
|
|
141
|
+
description: prop['description'],
|
|
142
|
+
items: prop['items']
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def generate_operation_id(method, path)
|
|
148
|
+
# Generate operation ID from method and path
|
|
149
|
+
# e.g., GET /users/{id} -> getUserById
|
|
150
|
+
path_parts = path.split('/').reject(&:empty?)
|
|
151
|
+
path_name = path_parts.map do |part|
|
|
152
|
+
part.start_with?('{') ? "by_#{part.tr('{}', '')}" : part
|
|
153
|
+
end.join('_')
|
|
154
|
+
|
|
155
|
+
"#{method}_#{path_name}".downcase
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* <%= parser.api_title %>
|
|
3
|
+
* <%= parser.api_description %>
|
|
4
|
+
* Version: <%= parser.api_version %>
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class APIClient {
|
|
8
|
+
constructor(baseUrl = '<%= parser.base_url %>', apiKey = null) {
|
|
9
|
+
this.baseUrl = baseUrl;
|
|
10
|
+
this.headers = {
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
'Accept': 'application/json'
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
if (apiKey) {
|
|
16
|
+
this.headers['Authorization'] = `Bearer ${apiKey}`;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
<% parser.endpoints.each do |endpoint| %>
|
|
21
|
+
/**
|
|
22
|
+
* <%= endpoint[:summary] %>
|
|
23
|
+
* <%= endpoint[:description] %>
|
|
24
|
+
<%- params = endpoint[:parameters] || [] -%>
|
|
25
|
+
<%- params.each do |param| -%>
|
|
26
|
+
* @param {<%= js_type(param[:type]) %>} <%= sanitize_name(param[:name]) %> - <%= param[:description] %>
|
|
27
|
+
<%- end -%>
|
|
28
|
+
<%- if endpoint[:request_body] -%>
|
|
29
|
+
* @param {object} body - Request body
|
|
30
|
+
<%- end -%>
|
|
31
|
+
* @returns {Promise<object>}
|
|
32
|
+
*/
|
|
33
|
+
async <%= js_method_name(endpoint) %>(<%= format_js_params(endpoint) %>) {
|
|
34
|
+
<%- path_params = params.select { |p| p[:location] == 'path' } -%>
|
|
35
|
+
<%- query_params = params.select { |p| p[:location] == 'query' } -%>
|
|
36
|
+
<%- if path_params.any? -%>
|
|
37
|
+
let path = '<%= endpoint[:path] %>';
|
|
38
|
+
<%- path_params.each do |param| -%>
|
|
39
|
+
path = path.replace('{<%= param[:name] %>}', <%= sanitize_name(param[:name]) %>);
|
|
40
|
+
<%- end -%>
|
|
41
|
+
<%- else -%>
|
|
42
|
+
let path = '<%= endpoint[:path] %>';
|
|
43
|
+
<%- end -%>
|
|
44
|
+
|
|
45
|
+
<%- if query_params.any? -%>
|
|
46
|
+
const queryParams = new URLSearchParams();
|
|
47
|
+
<%- query_params.each do |param| -%>
|
|
48
|
+
if (<%= sanitize_name(param[:name]) %> !== undefined && <%= sanitize_name(param[:name]) %> !== null) {
|
|
49
|
+
queryParams.append('<%= param[:name] %>', <%= sanitize_name(param[:name]) %>);
|
|
50
|
+
}
|
|
51
|
+
<%- end -%>
|
|
52
|
+
|
|
53
|
+
const queryString = queryParams.toString();
|
|
54
|
+
if (queryString) {
|
|
55
|
+
path += `?${queryString}`;
|
|
56
|
+
}
|
|
57
|
+
<%- end -%>
|
|
58
|
+
|
|
59
|
+
return this.makeRequest('<%= endpoint[:method].upcase %>', path<%= endpoint[:request_body] ? ', body' : '' %>);
|
|
60
|
+
}
|
|
61
|
+
<% end %>
|
|
62
|
+
|
|
63
|
+
async makeRequest(method, path, body = null) {
|
|
64
|
+
const url = `${this.baseUrl}${path}`;
|
|
65
|
+
|
|
66
|
+
const options = {
|
|
67
|
+
method: method,
|
|
68
|
+
headers: this.headers
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
if (body) {
|
|
72
|
+
options.body = JSON.stringify(body);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const response = await fetch(url, options);
|
|
77
|
+
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
const errorText = await response.text();
|
|
80
|
+
throw new Error(`HTTP Error ${response.status}: ${errorText}`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const contentType = response.headers.get('content-type');
|
|
84
|
+
if (contentType && contentType.includes('application/json')) {
|
|
85
|
+
return await response.json();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return await response.text();
|
|
89
|
+
} catch (error) {
|
|
90
|
+
throw new Error(`Request failed: ${error.message}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = APIClient;
|
|
96
|
+
|
|
97
|
+
<%- def format_js_params(endpoint)
|
|
98
|
+
params = endpoint[:parameters] || []
|
|
99
|
+
param_list = params.map { |p| sanitize_name(p[:name]) }
|
|
100
|
+
param_list << 'body = null' if endpoint[:request_body]
|
|
101
|
+
param_list.join(', ')
|
|
102
|
+
end -%>
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
require 'net/http'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'uri'
|
|
4
|
+
|
|
5
|
+
class APIClient
|
|
6
|
+
attr_reader :base_url, :headers
|
|
7
|
+
|
|
8
|
+
def initialize(base_url = '<%= parser.base_url %>', api_key: nil)
|
|
9
|
+
@base_url = base_url
|
|
10
|
+
@headers = {
|
|
11
|
+
'Content-Type' => 'application/json',
|
|
12
|
+
'Accept' => 'application/json'
|
|
13
|
+
}
|
|
14
|
+
@headers['Authorization'] = "Bearer #{api_key}" if api_key
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
<% parser.endpoints.each do |endpoint| %>
|
|
18
|
+
# <%= endpoint[:summary] %>
|
|
19
|
+
# <%= endpoint[:description] %>
|
|
20
|
+
<%- params = endpoint[:parameters] || [] -%>
|
|
21
|
+
<%- query_params = params.select { |p| p[:location] == 'query' } -%>
|
|
22
|
+
<%- path_params = params.select { |p| p[:location] == 'path' } -%>
|
|
23
|
+
def <%= method_name(endpoint) %>(<%= format_method_params(endpoint) %>)
|
|
24
|
+
<%- if path_params.any? -%>
|
|
25
|
+
path = "<%= endpoint[:path] %>"
|
|
26
|
+
<%- path_params.each do |param| -%>
|
|
27
|
+
path = path.gsub('{<%= param[:name] %>}', <%= sanitize_name(param[:name]) %>.to_s)
|
|
28
|
+
<%- end -%>
|
|
29
|
+
<%- else -%>
|
|
30
|
+
path = "<%= endpoint[:path] %>"
|
|
31
|
+
<%- end -%>
|
|
32
|
+
|
|
33
|
+
<%- if query_params.any? -%>
|
|
34
|
+
query_params = {}
|
|
35
|
+
<%- query_params.each do |param| -%>
|
|
36
|
+
query_params['<%= param[:name] %>'] = <%= sanitize_name(param[:name]) %> if <%= sanitize_name(param[:name]) %>
|
|
37
|
+
<%- end -%>
|
|
38
|
+
path += "?#{URI.encode_www_form(query_params)}" unless query_params.empty?
|
|
39
|
+
<%- end -%>
|
|
40
|
+
|
|
41
|
+
make_request('<%= endpoint[:method] %>', path<%= endpoint[:request_body] ? ', body: body' : '' %>)
|
|
42
|
+
end
|
|
43
|
+
<% end %>
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def make_request(method, path, body: nil)
|
|
48
|
+
uri = URI.join(@base_url, path)
|
|
49
|
+
|
|
50
|
+
http = Net::HTTP.new(uri.host, uri.port)
|
|
51
|
+
http.use_ssl = uri.scheme == 'https'
|
|
52
|
+
|
|
53
|
+
request = case method.upcase
|
|
54
|
+
when 'GET'
|
|
55
|
+
Net::HTTP::Get.new(uri)
|
|
56
|
+
when 'POST'
|
|
57
|
+
Net::HTTP::Post.new(uri)
|
|
58
|
+
when 'PUT'
|
|
59
|
+
Net::HTTP::Put.new(uri)
|
|
60
|
+
when 'DELETE'
|
|
61
|
+
Net::HTTP::Delete.new(uri)
|
|
62
|
+
when 'PATCH'
|
|
63
|
+
Net::HTTP::Patch.new(uri)
|
|
64
|
+
else
|
|
65
|
+
raise "Unsupported HTTP method: #{method}"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
@headers.each { |key, value| request[key] = value }
|
|
69
|
+
request.body = body.to_json if body
|
|
70
|
+
|
|
71
|
+
response = http.request(request)
|
|
72
|
+
|
|
73
|
+
case response
|
|
74
|
+
when Net::HTTPSuccess
|
|
75
|
+
response.body.empty? ? {} : JSON.parse(response.body)
|
|
76
|
+
else
|
|
77
|
+
raise "HTTP Error #{response.code}: #{response.body}"
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
<%- def format_method_params(endpoint)
|
|
83
|
+
params = endpoint[:parameters] || []
|
|
84
|
+
param_list = params.map { |p| sanitize_name(p[:name]) }
|
|
85
|
+
param_list << 'body: nil' if endpoint[:request_body]
|
|
86
|
+
param_list.join(', ')
|
|
87
|
+
end -%>
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# <%= @current_model[:name] %>
|
|
2
|
+
class <%= camelize(@current_model[:name]) %>
|
|
3
|
+
<%- @current_model[:properties].each do |name, prop| -%>
|
|
4
|
+
attr_accessor :<%= sanitize_name(name) %>
|
|
5
|
+
<%- end -%>
|
|
6
|
+
|
|
7
|
+
def initialize(attributes = {})
|
|
8
|
+
<%- @current_model[:properties].each do |name, prop| -%>
|
|
9
|
+
@<%= sanitize_name(name) %> = attributes['<%= name %>'] || attributes[:<%= sanitize_name(name) %>]
|
|
10
|
+
<%- end -%>
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def to_h
|
|
14
|
+
{
|
|
15
|
+
<%- @current_model[:properties].keys.each_with_index do |name, index| -%>
|
|
16
|
+
'<%= name %>' => @<%= sanitize_name(name) %><%= index < @current_model[:properties].keys.length - 1 ? ',' : '' %>
|
|
17
|
+
<%- end -%>
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def to_json(*args)
|
|
22
|
+
to_h.to_json(*args)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.from_json(json)
|
|
26
|
+
data = JSON.parse(json)
|
|
27
|
+
new(data)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
require 'yaml'
|
|
3
|
+
require 'erb'
|
|
4
|
+
require 'fileutils'
|
|
5
|
+
|
|
6
|
+
require_relative 'openapi_sdk_generator/parser'
|
|
7
|
+
require_relative 'openapi_sdk_generator/generator'
|
|
8
|
+
require_relative 'openapi_sdk_generator/generators/ruby_generator'
|
|
9
|
+
require_relative 'openapi_sdk_generator/generators/javascript_generator'
|
|
10
|
+
|
|
11
|
+
module OpenapiSdkGenerator
|
|
12
|
+
class Error < StandardError; end
|
|
13
|
+
|
|
14
|
+
class CLI
|
|
15
|
+
def initialize(options)
|
|
16
|
+
@options = options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def run
|
|
20
|
+
validate_options!
|
|
21
|
+
|
|
22
|
+
puts " Parsing OpenAPI specification..."
|
|
23
|
+
parser = Parser.new(@options[:input])
|
|
24
|
+
|
|
25
|
+
puts "🔨 Generating #{@options[:language]} SDK..."
|
|
26
|
+
generator = create_generator(@options[:language], parser)
|
|
27
|
+
|
|
28
|
+
puts " Writing files to #{@options[:output]}..."
|
|
29
|
+
generator.write_to_directory(@options[:output])
|
|
30
|
+
|
|
31
|
+
puts " SDK generated successfully!"
|
|
32
|
+
puts "📁 Output directory: #{@options[:output]}"
|
|
33
|
+
rescue => e
|
|
34
|
+
puts " Error: #{e.message}"
|
|
35
|
+
exit 1
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def validate_options!
|
|
41
|
+
raise Error, "Input file is required" unless @options[:input]
|
|
42
|
+
raise Error, "Output directory is required" unless @options[:output]
|
|
43
|
+
raise Error, "Language is required (ruby or javascript)" unless @options[:language]
|
|
44
|
+
raise Error, "Input file not found" unless File.exist?(@options[:input])
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def create_generator(language, parser)
|
|
48
|
+
case language.downcase
|
|
49
|
+
when 'ruby'
|
|
50
|
+
Generators::RubyGenerator.new(parser)
|
|
51
|
+
when 'javascript', 'js'
|
|
52
|
+
Generators::JavascriptGenerator.new(parser)
|
|
53
|
+
else
|
|
54
|
+
raise Error, "Unsupported language: #{language}"
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: taimour_openapi_sdk_generator
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Taimour
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: rspec
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - "~>"
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '3.12'
|
|
33
|
+
type: :development
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - "~>"
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '3.12'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: rake
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - "~>"
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '13.0'
|
|
47
|
+
type: :development
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - "~>"
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '13.0'
|
|
54
|
+
description: A lightweight tool to generate client SDKs in multiple languages from
|
|
55
|
+
OpenAPI/Swagger specs
|
|
56
|
+
email: taimour.ffcb@gmail.com
|
|
57
|
+
executables:
|
|
58
|
+
- openapi-sdk-generator
|
|
59
|
+
extensions: []
|
|
60
|
+
extra_rdoc_files: []
|
|
61
|
+
files:
|
|
62
|
+
- bin/openapi-sdk-generator
|
|
63
|
+
- lib/openapi_sdk_generator.rb
|
|
64
|
+
- lib/openapi_sdk_generator/generator.rb
|
|
65
|
+
- lib/openapi_sdk_generator/generators/javascript_generator.rb
|
|
66
|
+
- lib/openapi_sdk_generator/generators/ruby_generator.rb
|
|
67
|
+
- lib/openapi_sdk_generator/parser.rb
|
|
68
|
+
- lib/openapi_sdk_generator/templates/javascript_client.erb
|
|
69
|
+
- lib/openapi_sdk_generator/templates/ruby_client.erb
|
|
70
|
+
- lib/openapi_sdk_generator/templates/ruby_model.erb
|
|
71
|
+
homepage: https://github.com/yourusername/openapi_sdk_generator
|
|
72
|
+
licenses:
|
|
73
|
+
- MIT
|
|
74
|
+
metadata: {}
|
|
75
|
+
rdoc_options: []
|
|
76
|
+
require_paths:
|
|
77
|
+
- lib
|
|
78
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: 2.7.0
|
|
83
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
84
|
+
requirements:
|
|
85
|
+
- - ">="
|
|
86
|
+
- !ruby/object:Gem::Version
|
|
87
|
+
version: '0'
|
|
88
|
+
requirements: []
|
|
89
|
+
rubygems_version: 3.6.9
|
|
90
|
+
specification_version: 4
|
|
91
|
+
summary: Generate SDKs from OpenAPI specifications
|
|
92
|
+
test_files: []
|