rapitapir 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/.rspec +3 -0
- data/.rubocop.yml +57 -0
- data/CHANGELOG.md +94 -0
- data/CLEANUP_SUMMARY.md +155 -0
- data/CONTRIBUTING.md +280 -0
- data/LICENSE +21 -0
- data/README.md +485 -0
- data/debug_hash.rb +20 -0
- data/docs/EXTENSION_COMPARISON.md +388 -0
- data/docs/SINATRA_EXTENSION.md +467 -0
- data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
- data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
- data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
- data/docs/archive/PHASE_2_SUMMARY.md +209 -0
- data/docs/archive/REFACTORING_SUMMARY.md +184 -0
- data/docs/archive/phase_1_3_plan.md +136 -0
- data/docs/archive/sinatra_extension_summary.md +188 -0
- data/docs/archive/sinatra_working_solution.md +113 -0
- data/docs/archive/typescript-client-generator-summary.md +259 -0
- data/docs/auto-derivation.md +146 -0
- data/docs/blueprint.md +1091 -0
- data/docs/endpoint-definition.md +211 -0
- data/docs/github_pages_fix.md +52 -0
- data/docs/github_pages_setup.md +49 -0
- data/docs/implementation-status.md +357 -0
- data/docs/observability.md +647 -0
- data/docs/phase3-plan.md +108 -0
- data/docs/sinatra_rapitapir.md +87 -0
- data/docs/type_shortcuts.md +146 -0
- data/examples/README_ENTERPRISE.md +202 -0
- data/examples/authentication_example.rb +192 -0
- data/examples/auto_derivation_ruby_friendly.rb +163 -0
- data/examples/cli/user_api_endpoints.rb +56 -0
- data/examples/client/typescript_client_example.rb +102 -0
- data/examples/client/user-api-client.ts +193 -0
- data/examples/demo_api.rb +41 -0
- data/examples/docs/documentation_example.rb +112 -0
- data/examples/docs/user-api-docs.html +789 -0
- data/examples/docs/user-api-docs.md +403 -0
- data/examples/enhanced_auto_derivation_test.rb +83 -0
- data/examples/enterprise_extension_demo.rb +417 -0
- data/examples/enterprise_rapitapir_api.rb +662 -0
- data/examples/getting_started_extension.rb +218 -0
- data/examples/hello_world.rb +74 -0
- data/examples/oauth2/.env.example +19 -0
- data/examples/oauth2/README.md +205 -0
- data/examples/oauth2/generic_oauth2_api.rb +226 -0
- data/examples/oauth2/get_token.rb +72 -0
- data/examples/oauth2/songs_api_with_auth0.rb +320 -0
- data/examples/oauth2/test_api.sh +16 -0
- data/examples/oauth2/test_songs_api.sh +110 -0
- data/examples/observability/.env.example +35 -0
- data/examples/observability/README.md +230 -0
- data/examples/observability/README_HONEYCOMB.md +332 -0
- data/examples/observability/advanced_setup.rb +384 -0
- data/examples/observability/basic_setup.rb +192 -0
- data/examples/observability/complete_test.rb +121 -0
- data/examples/observability/honeycomb_example.rb +523 -0
- data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
- data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
- data/examples/observability/honeycomb_working_example.rb +489 -0
- data/examples/observability/quick_test.rb +78 -0
- data/examples/observability/simple_test.rb +14 -0
- data/examples/observability/test_honeycomb_demo.rb +354 -0
- data/examples/observability/test_live_honeycomb.rb +111 -0
- data/examples/observability/test_validation.rb +78 -0
- data/examples/observability/test_working_validation.rb +66 -0
- data/examples/openapi/user_api_schema.rb +132 -0
- data/examples/production_ready_example.rb +105 -0
- data/examples/rails/users_controller.rb +146 -0
- data/examples/readme/basic_sinatra_example.rb +128 -0
- data/examples/server/user_api.rb +179 -0
- data/examples/simple_auto_derivation_demo.rb +44 -0
- data/examples/simple_demo_api.rb +18 -0
- data/examples/sinatra/user_app.rb +127 -0
- data/examples/t_shortcut_demo.rb +59 -0
- data/examples/user_api.rb +190 -0
- data/examples/working_getting_started.rb +184 -0
- data/examples/working_simple_example.rb +195 -0
- data/lib/rapitapir/auth/configuration.rb +129 -0
- data/lib/rapitapir/auth/context.rb +122 -0
- data/lib/rapitapir/auth/errors.rb +104 -0
- data/lib/rapitapir/auth/middleware.rb +324 -0
- data/lib/rapitapir/auth/oauth2.rb +350 -0
- data/lib/rapitapir/auth/schemes.rb +420 -0
- data/lib/rapitapir/auth.rb +113 -0
- data/lib/rapitapir/cli/command.rb +535 -0
- data/lib/rapitapir/cli/server.rb +243 -0
- data/lib/rapitapir/cli/validator.rb +373 -0
- data/lib/rapitapir/client/generator_base.rb +272 -0
- data/lib/rapitapir/client/typescript_generator.rb +350 -0
- data/lib/rapitapir/core/endpoint.rb +158 -0
- data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
- data/lib/rapitapir/core/input.rb +182 -0
- data/lib/rapitapir/core/output.rb +164 -0
- data/lib/rapitapir/core/request.rb +19 -0
- data/lib/rapitapir/core/response.rb +17 -0
- data/lib/rapitapir/docs/html_generator.rb +780 -0
- data/lib/rapitapir/docs/markdown_generator.rb +464 -0
- data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
- data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
- data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
- data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
- data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
- data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
- data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
- data/lib/rapitapir/dsl/http_verbs.rb +77 -0
- data/lib/rapitapir/dsl/input_methods.rb +47 -0
- data/lib/rapitapir/dsl/observability_methods.rb +81 -0
- data/lib/rapitapir/dsl/output_methods.rb +43 -0
- data/lib/rapitapir/dsl/type_resolution.rb +43 -0
- data/lib/rapitapir/observability/configuration.rb +108 -0
- data/lib/rapitapir/observability/health_check.rb +236 -0
- data/lib/rapitapir/observability/logging.rb +270 -0
- data/lib/rapitapir/observability/metrics.rb +203 -0
- data/lib/rapitapir/observability/middleware.rb +243 -0
- data/lib/rapitapir/observability/tracing.rb +143 -0
- data/lib/rapitapir/observability.rb +28 -0
- data/lib/rapitapir/openapi/schema_generator.rb +403 -0
- data/lib/rapitapir/schema.rb +136 -0
- data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
- data/lib/rapitapir/server/middleware.rb +120 -0
- data/lib/rapitapir/server/path_matcher.rb +45 -0
- data/lib/rapitapir/server/rack_adapter.rb +215 -0
- data/lib/rapitapir/server/rails_adapter.rb +17 -0
- data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
- data/lib/rapitapir/server/rails_controller.rb +72 -0
- data/lib/rapitapir/server/rails_input_processor.rb +73 -0
- data/lib/rapitapir/server/rails_response_handler.rb +29 -0
- data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
- data/lib/rapitapir/server/sinatra_integration.rb +93 -0
- data/lib/rapitapir/sinatra/configuration.rb +91 -0
- data/lib/rapitapir/sinatra/extension.rb +214 -0
- data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
- data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
- data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
- data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
- data/lib/rapitapir/types/array.rb +163 -0
- data/lib/rapitapir/types/auto_derivation.rb +265 -0
- data/lib/rapitapir/types/base.rb +146 -0
- data/lib/rapitapir/types/boolean.rb +46 -0
- data/lib/rapitapir/types/date.rb +92 -0
- data/lib/rapitapir/types/datetime.rb +98 -0
- data/lib/rapitapir/types/email.rb +32 -0
- data/lib/rapitapir/types/float.rb +134 -0
- data/lib/rapitapir/types/hash.rb +161 -0
- data/lib/rapitapir/types/integer.rb +143 -0
- data/lib/rapitapir/types/object.rb +156 -0
- data/lib/rapitapir/types/optional.rb +65 -0
- data/lib/rapitapir/types/string.rb +185 -0
- data/lib/rapitapir/types/uuid.rb +32 -0
- data/lib/rapitapir/types.rb +155 -0
- data/lib/rapitapir/version.rb +5 -0
- data/lib/rapitapir.rb +173 -0
- data/rapitapir.gemspec +66 -0
- metadata +387 -0
@@ -0,0 +1,272 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Client
|
5
|
+
# Base class for client code generators
|
6
|
+
# Provides common functionality for generating API clients in different languages
|
7
|
+
class GeneratorBase
|
8
|
+
attr_reader :endpoints, :config
|
9
|
+
|
10
|
+
def initialize(endpoints: [], config: {})
|
11
|
+
@endpoints = endpoints
|
12
|
+
@config = default_config.merge(config)
|
13
|
+
end
|
14
|
+
|
15
|
+
# Generate client code - to be implemented by subclasses
|
16
|
+
def generate
|
17
|
+
raise NotImplementedError, 'Subclasses must implement #generate'
|
18
|
+
end
|
19
|
+
|
20
|
+
# Save generated client to file
|
21
|
+
def save_to_file(filename)
|
22
|
+
content = generate
|
23
|
+
File.write(filename, content)
|
24
|
+
puts "Client saved to #{filename}"
|
25
|
+
end
|
26
|
+
|
27
|
+
protected
|
28
|
+
|
29
|
+
def default_config
|
30
|
+
{
|
31
|
+
base_url: 'http://localhost:4567',
|
32
|
+
client_name: 'ApiClient',
|
33
|
+
package_name: 'api-client',
|
34
|
+
version: '1.0.0'
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Convert RapiTapir types to language-specific types
|
39
|
+
def convert_type(type, language:)
|
40
|
+
case language
|
41
|
+
when :typescript
|
42
|
+
convert_to_typescript_type(type)
|
43
|
+
when :python
|
44
|
+
convert_to_python_type(type)
|
45
|
+
else
|
46
|
+
type.to_s
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def convert_to_typescript_type(type)
|
51
|
+
case type
|
52
|
+
when :string, String, RapiTapir::Types::String
|
53
|
+
'string'
|
54
|
+
when :integer, Integer, RapiTapir::Types::Integer, :number, Float, RapiTapir::Types::Float
|
55
|
+
'number'
|
56
|
+
when :boolean, RapiTapir::Types::Boolean
|
57
|
+
'boolean'
|
58
|
+
when :date, :datetime, RapiTapir::Types::Date, RapiTapir::Types::DateTime
|
59
|
+
'Date'
|
60
|
+
else
|
61
|
+
convert_complex_typescript_type(type)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
def convert_complex_typescript_type(type)
|
66
|
+
case type
|
67
|
+
when RapiTapir::Types::Array
|
68
|
+
convert_array_type_to_typescript(type)
|
69
|
+
when RapiTapir::Types::Hash, Hash
|
70
|
+
convert_hash_type_to_typescript(type)
|
71
|
+
when Array
|
72
|
+
convert_ruby_array_to_typescript(type)
|
73
|
+
else
|
74
|
+
'any'
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def convert_array_type_to_typescript(type)
|
79
|
+
"#{convert_to_typescript_type(type.item_type)}[]"
|
80
|
+
end
|
81
|
+
|
82
|
+
def convert_hash_type_to_typescript(type)
|
83
|
+
if hash_type_empty?(type)
|
84
|
+
'Record<string, any>'
|
85
|
+
else
|
86
|
+
convert_hash_properties_to_typescript(type)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
def hash_type_empty?(type)
|
91
|
+
(type.respond_to?(:field_types) && type.field_types.empty?) ||
|
92
|
+
(type.respond_to?(:empty?) && type.empty?)
|
93
|
+
end
|
94
|
+
|
95
|
+
def convert_hash_properties_to_typescript(type)
|
96
|
+
properties = if type.respond_to?(:field_types)
|
97
|
+
convert_field_types_to_typescript(type.field_types)
|
98
|
+
else
|
99
|
+
convert_hash_entries_to_typescript(type)
|
100
|
+
end
|
101
|
+
"{ #{properties.join('; ')} }"
|
102
|
+
end
|
103
|
+
|
104
|
+
def convert_field_types_to_typescript(field_types)
|
105
|
+
field_types.map do |key, value|
|
106
|
+
"#{key}: #{convert_to_typescript_type(value)}"
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def convert_hash_entries_to_typescript(hash)
|
111
|
+
hash.map do |key, value|
|
112
|
+
"#{key}: #{convert_to_typescript_type(value)}"
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def convert_ruby_array_to_typescript(type)
|
117
|
+
if type.length == 1
|
118
|
+
"#{convert_to_typescript_type(type.first)}[]"
|
119
|
+
else
|
120
|
+
'any[]'
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def convert_to_python_type(type)
|
125
|
+
case type
|
126
|
+
when :string, String
|
127
|
+
'str'
|
128
|
+
when :integer, Integer
|
129
|
+
'int'
|
130
|
+
when :number, Float
|
131
|
+
'float'
|
132
|
+
when :boolean
|
133
|
+
'bool'
|
134
|
+
else
|
135
|
+
convert_complex_python_type(type)
|
136
|
+
end
|
137
|
+
end
|
138
|
+
|
139
|
+
def convert_complex_python_type(type)
|
140
|
+
case type
|
141
|
+
when :date, :datetime
|
142
|
+
'datetime'
|
143
|
+
when Array
|
144
|
+
convert_array_to_python_type(type)
|
145
|
+
when Hash
|
146
|
+
convert_hash_to_python_type(type)
|
147
|
+
else
|
148
|
+
'Any'
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
def convert_array_to_python_type(type)
|
153
|
+
if type.length == 1
|
154
|
+
"List[#{convert_to_python_type(type.first)}]"
|
155
|
+
else
|
156
|
+
'List[Any]'
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
def convert_hash_to_python_type(type)
|
161
|
+
return 'Dict[str, Any]' if type.empty?
|
162
|
+
|
163
|
+
'Dict[str, Any]'
|
164
|
+
end
|
165
|
+
|
166
|
+
# Generate HTTP method name
|
167
|
+
def method_name_for_endpoint(endpoint)
|
168
|
+
method = endpoint.method.to_s.downcase
|
169
|
+
path_parts = extract_static_path_parts(endpoint.path)
|
170
|
+
|
171
|
+
generate_method_name_by_http_method(method, endpoint.path, path_parts)
|
172
|
+
end
|
173
|
+
|
174
|
+
def extract_static_path_parts(path)
|
175
|
+
path.split('/').reject(&:empty?).map do |part|
|
176
|
+
part.start_with?(':') ? nil : part
|
177
|
+
end.compact
|
178
|
+
end
|
179
|
+
|
180
|
+
def generate_method_name_by_http_method(method, path, path_parts)
|
181
|
+
case method
|
182
|
+
when 'get'
|
183
|
+
generate_get_method_name(path, path_parts)
|
184
|
+
when 'post'
|
185
|
+
generate_post_method_name(path_parts)
|
186
|
+
when 'put'
|
187
|
+
generate_put_method_name(path_parts)
|
188
|
+
when 'delete'
|
189
|
+
generate_delete_method_name(path_parts)
|
190
|
+
else
|
191
|
+
generate_default_method_name(method, path_parts)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def generate_get_method_name(path, path_parts)
|
196
|
+
if path.include?(':')
|
197
|
+
# GET /users/:id -> getUserById
|
198
|
+
base_name = path_parts.map(&:capitalize).join
|
199
|
+
"get#{base_name}ById"
|
200
|
+
else
|
201
|
+
# GET /users -> getUsers
|
202
|
+
"get#{path_parts.map(&:capitalize).join}"
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def generate_post_method_name(path_parts)
|
207
|
+
# POST /users -> createUser
|
208
|
+
singular_name = get_singular_name(path_parts)
|
209
|
+
"create#{singular_name}"
|
210
|
+
end
|
211
|
+
|
212
|
+
def generate_put_method_name(path_parts)
|
213
|
+
# PUT /users/:id -> updateUser
|
214
|
+
singular_name = get_singular_name(path_parts)
|
215
|
+
"update#{singular_name}"
|
216
|
+
end
|
217
|
+
|
218
|
+
def generate_delete_method_name(path_parts)
|
219
|
+
# DELETE /users/:id -> deleteUser
|
220
|
+
singular_name = get_singular_name(path_parts)
|
221
|
+
"delete#{singular_name}"
|
222
|
+
end
|
223
|
+
|
224
|
+
def generate_default_method_name(method, path_parts)
|
225
|
+
"#{method}#{path_parts.map(&:capitalize).join}"
|
226
|
+
end
|
227
|
+
|
228
|
+
def get_singular_name(path_parts)
|
229
|
+
return path_parts.map(&:capitalize).join unless path_parts.any?
|
230
|
+
|
231
|
+
singular_name = singularize(path_parts.last)
|
232
|
+
singular_name&.capitalize || path_parts.map(&:capitalize).join
|
233
|
+
end
|
234
|
+
|
235
|
+
# Simple singularize method (basic implementation)
|
236
|
+
def singularize(word)
|
237
|
+
return nil unless word
|
238
|
+
|
239
|
+
word = word.to_s
|
240
|
+
case word
|
241
|
+
when /ies$/
|
242
|
+
word.sub(/ies$/, 'y')
|
243
|
+
when /s$/
|
244
|
+
word.sub(/s$/, '')
|
245
|
+
else
|
246
|
+
word
|
247
|
+
end
|
248
|
+
end
|
249
|
+
|
250
|
+
# Extract path parameters from endpoint
|
251
|
+
def path_parameters(endpoint)
|
252
|
+
endpoint.inputs.select { |input| input.kind == :path }
|
253
|
+
end
|
254
|
+
|
255
|
+
# Extract query parameters from endpoint
|
256
|
+
def query_parameters(endpoint)
|
257
|
+
endpoint.inputs.select { |input| input.kind == :query }
|
258
|
+
end
|
259
|
+
|
260
|
+
# Extract request body from endpoint
|
261
|
+
def request_body(endpoint)
|
262
|
+
endpoint.inputs.find { |input| input.kind == :body }
|
263
|
+
end
|
264
|
+
|
265
|
+
# Get response type from endpoint
|
266
|
+
def response_type(endpoint)
|
267
|
+
output = endpoint.outputs.first
|
268
|
+
output&.type
|
269
|
+
end
|
270
|
+
end
|
271
|
+
end
|
272
|
+
end
|
@@ -0,0 +1,350 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'generator_base'
|
4
|
+
|
5
|
+
module RapiTapir
|
6
|
+
module Client
|
7
|
+
# TypeScript client generator for RapiTapir APIs
|
8
|
+
# Generates strongly-typed TypeScript clients from endpoint definitions
|
9
|
+
class TypescriptGenerator < GeneratorBase
|
10
|
+
def generate
|
11
|
+
[
|
12
|
+
generate_header,
|
13
|
+
generate_types,
|
14
|
+
generate_client_class,
|
15
|
+
generate_exports
|
16
|
+
].join("\n\n")
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def generate_header
|
22
|
+
<<~TYPESCRIPT
|
23
|
+
// Generated by RapiTapir TypeScript Client Generator
|
24
|
+
// Package: #{config[:package_name]}
|
25
|
+
// Version: #{config[:version]}
|
26
|
+
// Base URL: #{config[:base_url]}
|
27
|
+
|
28
|
+
// Response type for all API calls
|
29
|
+
export interface ApiResponse<T> {
|
30
|
+
data: T;
|
31
|
+
status: number;
|
32
|
+
headers: Record<string, string>;
|
33
|
+
}
|
34
|
+
|
35
|
+
// Error type for API errors
|
36
|
+
export interface ApiError {
|
37
|
+
message: string;
|
38
|
+
status: number;
|
39
|
+
details?: any;
|
40
|
+
}
|
41
|
+
|
42
|
+
// HTTP client configuration
|
43
|
+
export interface ClientConfig {
|
44
|
+
baseUrl?: string;
|
45
|
+
headers?: Record<string, string>;
|
46
|
+
timeout?: number;
|
47
|
+
}
|
48
|
+
TYPESCRIPT
|
49
|
+
end
|
50
|
+
|
51
|
+
def generate_types
|
52
|
+
# Generate request/response types for each endpoint
|
53
|
+
types = endpoints.map do |endpoint|
|
54
|
+
generate_endpoint_types(endpoint)
|
55
|
+
end
|
56
|
+
|
57
|
+
return '' if types.empty?
|
58
|
+
|
59
|
+
"// Generated types\n#{types.compact.join("\n\n")}"
|
60
|
+
end
|
61
|
+
|
62
|
+
def generate_endpoint_types(endpoint)
|
63
|
+
types = []
|
64
|
+
method_name = method_name_for_endpoint(endpoint)
|
65
|
+
|
66
|
+
types.concat(generate_request_type(endpoint, method_name))
|
67
|
+
types.concat(generate_response_type(endpoint, method_name))
|
68
|
+
|
69
|
+
types.join("\n\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def generate_request_type(endpoint, method_name)
|
73
|
+
params = path_parameters(endpoint) + query_parameters(endpoint)
|
74
|
+
body = request_body(endpoint)
|
75
|
+
|
76
|
+
return [] unless params.any? || body
|
77
|
+
|
78
|
+
request_props = build_request_properties(endpoint, body)
|
79
|
+
return [] unless request_props.any?
|
80
|
+
|
81
|
+
[<<~TYPESCRIPT]
|
82
|
+
export interface #{method_name.capitalize}Request {
|
83
|
+
#{request_props.join("\n")}
|
84
|
+
}
|
85
|
+
TYPESCRIPT
|
86
|
+
end
|
87
|
+
|
88
|
+
def build_request_properties(endpoint, body)
|
89
|
+
props = []
|
90
|
+
|
91
|
+
props.concat(build_path_parameter_properties(endpoint))
|
92
|
+
props.concat(build_query_parameter_properties(endpoint))
|
93
|
+
props.concat(build_body_properties(body))
|
94
|
+
|
95
|
+
props
|
96
|
+
end
|
97
|
+
|
98
|
+
def build_path_parameter_properties(endpoint)
|
99
|
+
path_parameters(endpoint).map do |param|
|
100
|
+
ts_type = convert_type(param.type, language: :typescript)
|
101
|
+
" #{param.name}: #{ts_type};"
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def build_query_parameter_properties(endpoint)
|
106
|
+
query_parameters(endpoint).map do |param|
|
107
|
+
ts_type = convert_type(param.type, language: :typescript)
|
108
|
+
optional = param.required? ? '' : '?'
|
109
|
+
" #{param.name}#{optional}: #{ts_type};"
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def build_body_properties(body)
|
114
|
+
return [] unless body
|
115
|
+
|
116
|
+
ts_type = convert_type(body.type, language: :typescript)
|
117
|
+
[" body: #{ts_type};"]
|
118
|
+
end
|
119
|
+
|
120
|
+
def generate_response_type(endpoint, method_name)
|
121
|
+
response = response_type(endpoint)
|
122
|
+
return [] unless response
|
123
|
+
|
124
|
+
ts_type = convert_type(response, language: :typescript)
|
125
|
+
["export type #{method_name.capitalize}Response = #{ts_type};"]
|
126
|
+
end
|
127
|
+
|
128
|
+
def generate_client_class
|
129
|
+
methods = endpoints.map { |endpoint| generate_client_method(endpoint) }.compact
|
130
|
+
|
131
|
+
<<~TYPESCRIPT
|
132
|
+
export class #{config[:client_name]} {
|
133
|
+
private baseUrl: string;
|
134
|
+
private headers: Record<string, string>;
|
135
|
+
private timeout: number;
|
136
|
+
#{' '}
|
137
|
+
constructor(config: ClientConfig = {}) {
|
138
|
+
this.baseUrl = config.baseUrl || '#{config[:base_url]}';
|
139
|
+
this.headers = config.headers || {};
|
140
|
+
this.timeout = config.timeout || 10000;
|
141
|
+
}
|
142
|
+
#{' '}
|
143
|
+
private async request<T>(
|
144
|
+
method: string,
|
145
|
+
path: string,
|
146
|
+
options: {
|
147
|
+
params?: Record<string, any>;
|
148
|
+
body?: any;
|
149
|
+
headers?: Record<string, string>;
|
150
|
+
} = {}
|
151
|
+
): Promise<ApiResponse<T>> {
|
152
|
+
const url = new URL(path, this.baseUrl);
|
153
|
+
#{' '}
|
154
|
+
// Add query parameters
|
155
|
+
if (options.params) {
|
156
|
+
Object.entries(options.params).forEach(([key, value]) => {
|
157
|
+
if (value !== undefined && value !== null) {
|
158
|
+
url.searchParams.append(key, String(value));
|
159
|
+
}
|
160
|
+
});
|
161
|
+
}
|
162
|
+
#{' '}
|
163
|
+
const requestHeaders = {
|
164
|
+
'Content-Type': 'application/json',
|
165
|
+
...this.headers,
|
166
|
+
...options.headers,
|
167
|
+
};
|
168
|
+
#{' '}
|
169
|
+
const requestInit: RequestInit = {
|
170
|
+
method,
|
171
|
+
headers: requestHeaders,
|
172
|
+
};
|
173
|
+
#{' '}
|
174
|
+
if (options.body) {
|
175
|
+
requestInit.body = JSON.stringify(options.body);
|
176
|
+
}
|
177
|
+
#{' '}
|
178
|
+
try {
|
179
|
+
const response = await fetch(url.toString(), requestInit);
|
180
|
+
#{' '}
|
181
|
+
const responseHeaders: Record<string, string> = {};
|
182
|
+
response.headers.forEach((value, key) => {
|
183
|
+
responseHeaders[key] = value;
|
184
|
+
});
|
185
|
+
#{' '}
|
186
|
+
let data: T;
|
187
|
+
const contentType = response.headers.get('content-type');
|
188
|
+
if (contentType && contentType.includes('application/json')) {
|
189
|
+
data = await response.json();
|
190
|
+
} else {
|
191
|
+
data = (await response.text()) as unknown as T;
|
192
|
+
}
|
193
|
+
#{' '}
|
194
|
+
if (!response.ok) {
|
195
|
+
const error: ApiError = {
|
196
|
+
message: `HTTP ${response.status}: ${response.statusText}`,
|
197
|
+
status: response.status,
|
198
|
+
details: data,
|
199
|
+
};
|
200
|
+
throw error;
|
201
|
+
}
|
202
|
+
#{' '}
|
203
|
+
return {
|
204
|
+
data,
|
205
|
+
status: response.status,
|
206
|
+
headers: responseHeaders,
|
207
|
+
};
|
208
|
+
} catch (error) {
|
209
|
+
if (error && typeof error === 'object' && 'status' in error) {
|
210
|
+
throw error; // Re-throw ApiError
|
211
|
+
}
|
212
|
+
#{' '}
|
213
|
+
const apiError: ApiError = {
|
214
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
215
|
+
status: 0,
|
216
|
+
details: error,
|
217
|
+
};
|
218
|
+
throw apiError;
|
219
|
+
}
|
220
|
+
}
|
221
|
+
#{' '}
|
222
|
+
#{methods.join("\n\n")}
|
223
|
+
}
|
224
|
+
TYPESCRIPT
|
225
|
+
end
|
226
|
+
|
227
|
+
def generate_client_method(endpoint)
|
228
|
+
method_name = method_name_for_endpoint(endpoint)
|
229
|
+
http_method = endpoint.method.to_s.upcase
|
230
|
+
path = endpoint.path
|
231
|
+
|
232
|
+
params = build_method_parameters(endpoint, method_name)
|
233
|
+
has_params = method_parameters?(endpoint)
|
234
|
+
|
235
|
+
method_signature = build_method_signature(method_name, params, has_params, endpoint)
|
236
|
+
method_body = build_method_body(method_signature, endpoint, has_params, http_method, path)
|
237
|
+
|
238
|
+
method_body.join("\n")
|
239
|
+
end
|
240
|
+
|
241
|
+
def build_method_parameters(endpoint, method_name)
|
242
|
+
params = []
|
243
|
+
has_params = method_parameters?(endpoint)
|
244
|
+
params << "request: #{method_name.capitalize}Request" if has_params
|
245
|
+
params
|
246
|
+
end
|
247
|
+
|
248
|
+
def method_parameters?(endpoint)
|
249
|
+
path_parameters(endpoint).any? || query_parameters(endpoint).any? || request_body(endpoint)
|
250
|
+
end
|
251
|
+
|
252
|
+
def build_method_signature(method_name, params, has_params, endpoint)
|
253
|
+
response_type_name = determine_response_type_name(endpoint, method_name)
|
254
|
+
|
255
|
+
if has_params
|
256
|
+
"async #{method_name}(#{params.join(', ')}): Promise<ApiResponse<#{response_type_name}>>"
|
257
|
+
else
|
258
|
+
"async #{method_name}(): Promise<ApiResponse<#{response_type_name}>>"
|
259
|
+
end
|
260
|
+
end
|
261
|
+
|
262
|
+
def determine_response_type_name(endpoint, method_name)
|
263
|
+
if response_type(endpoint)
|
264
|
+
"#{method_name.capitalize}Response"
|
265
|
+
else
|
266
|
+
'void'
|
267
|
+
end
|
268
|
+
end
|
269
|
+
|
270
|
+
def build_method_body(method_signature, endpoint, has_params, http_method, path)
|
271
|
+
method_body = [" #{method_signature} {"]
|
272
|
+
|
273
|
+
if has_params
|
274
|
+
method_body.concat(build_method_body_with_params(endpoint, http_method, path))
|
275
|
+
else
|
276
|
+
method_body.concat(build_method_body_without_params(endpoint, http_method, path))
|
277
|
+
end
|
278
|
+
|
279
|
+
method_body << ' }'
|
280
|
+
method_body
|
281
|
+
end
|
282
|
+
|
283
|
+
def build_method_body_with_params(endpoint, http_method, path)
|
284
|
+
method_name = method_name_for_endpoint(endpoint)
|
285
|
+
response_type_name = determine_response_type_name(endpoint, method_name)
|
286
|
+
actual_path = build_actual_path(endpoint, path)
|
287
|
+
|
288
|
+
body = [" return this.request<#{response_type_name}>('#{http_method}', #{actual_path}, {"]
|
289
|
+
|
290
|
+
request_options = build_request_options(endpoint)
|
291
|
+
body << " #{request_options.join(',')}" if request_options.any?
|
292
|
+
body << ' });'
|
293
|
+
|
294
|
+
body
|
295
|
+
end
|
296
|
+
|
297
|
+
def build_method_body_without_params(endpoint, http_method, path)
|
298
|
+
method_name = method_name_for_endpoint(endpoint)
|
299
|
+
response_type_name = determine_response_type_name(endpoint, method_name)
|
300
|
+
[" return this.request<#{response_type_name}>('#{http_method}', '#{path}');"]
|
301
|
+
end
|
302
|
+
|
303
|
+
def build_actual_path(endpoint, path)
|
304
|
+
path_params = path_parameters(endpoint)
|
305
|
+
|
306
|
+
if path_params.any?
|
307
|
+
"`#{path.gsub(/:(\w+)/, '${request.\1}')}`"
|
308
|
+
else
|
309
|
+
"'#{path}'"
|
310
|
+
end
|
311
|
+
end
|
312
|
+
|
313
|
+
def build_request_options(endpoint)
|
314
|
+
options = []
|
315
|
+
|
316
|
+
query_params_obj = build_query_params_object(endpoint)
|
317
|
+
options << "params: #{query_params_obj}" if query_parameters(endpoint).any?
|
318
|
+
|
319
|
+
request_body_obj = build_request_body_object(endpoint)
|
320
|
+
options << "body: #{request_body_obj}" if request_body(endpoint)
|
321
|
+
|
322
|
+
options
|
323
|
+
end
|
324
|
+
|
325
|
+
def build_query_params_object(endpoint)
|
326
|
+
query_params = query_parameters(endpoint)
|
327
|
+
|
328
|
+
if query_params.any?
|
329
|
+
param_assignments = query_params.map do |param|
|
330
|
+
"#{param.name}: request.#{param.name}"
|
331
|
+
end
|
332
|
+
"{ #{param_assignments.join(', ')} }"
|
333
|
+
else
|
334
|
+
'undefined'
|
335
|
+
end
|
336
|
+
end
|
337
|
+
|
338
|
+
def build_request_body_object(endpoint)
|
339
|
+
request_body(endpoint) ? 'request.body' : 'undefined'
|
340
|
+
end
|
341
|
+
|
342
|
+
def generate_exports
|
343
|
+
<<~TYPESCRIPT
|
344
|
+
// Default export
|
345
|
+
export default #{config[:client_name]};
|
346
|
+
TYPESCRIPT
|
347
|
+
end
|
348
|
+
end
|
349
|
+
end
|
350
|
+
end
|