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.
Files changed (157) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +57 -0
  4. data/CHANGELOG.md +94 -0
  5. data/CLEANUP_SUMMARY.md +155 -0
  6. data/CONTRIBUTING.md +280 -0
  7. data/LICENSE +21 -0
  8. data/README.md +485 -0
  9. data/debug_hash.rb +20 -0
  10. data/docs/EXTENSION_COMPARISON.md +388 -0
  11. data/docs/SINATRA_EXTENSION.md +467 -0
  12. data/docs/archive/PHASE_1_2_COMPLETE.md +77 -0
  13. data/docs/archive/PHASE_1_3_COMPLETE.md +152 -0
  14. data/docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md +203 -0
  15. data/docs/archive/PHASE_2_SUMMARY.md +209 -0
  16. data/docs/archive/REFACTORING_SUMMARY.md +184 -0
  17. data/docs/archive/phase_1_3_plan.md +136 -0
  18. data/docs/archive/sinatra_extension_summary.md +188 -0
  19. data/docs/archive/sinatra_working_solution.md +113 -0
  20. data/docs/archive/typescript-client-generator-summary.md +259 -0
  21. data/docs/auto-derivation.md +146 -0
  22. data/docs/blueprint.md +1091 -0
  23. data/docs/endpoint-definition.md +211 -0
  24. data/docs/github_pages_fix.md +52 -0
  25. data/docs/github_pages_setup.md +49 -0
  26. data/docs/implementation-status.md +357 -0
  27. data/docs/observability.md +647 -0
  28. data/docs/phase3-plan.md +108 -0
  29. data/docs/sinatra_rapitapir.md +87 -0
  30. data/docs/type_shortcuts.md +146 -0
  31. data/examples/README_ENTERPRISE.md +202 -0
  32. data/examples/authentication_example.rb +192 -0
  33. data/examples/auto_derivation_ruby_friendly.rb +163 -0
  34. data/examples/cli/user_api_endpoints.rb +56 -0
  35. data/examples/client/typescript_client_example.rb +102 -0
  36. data/examples/client/user-api-client.ts +193 -0
  37. data/examples/demo_api.rb +41 -0
  38. data/examples/docs/documentation_example.rb +112 -0
  39. data/examples/docs/user-api-docs.html +789 -0
  40. data/examples/docs/user-api-docs.md +403 -0
  41. data/examples/enhanced_auto_derivation_test.rb +83 -0
  42. data/examples/enterprise_extension_demo.rb +417 -0
  43. data/examples/enterprise_rapitapir_api.rb +662 -0
  44. data/examples/getting_started_extension.rb +218 -0
  45. data/examples/hello_world.rb +74 -0
  46. data/examples/oauth2/.env.example +19 -0
  47. data/examples/oauth2/README.md +205 -0
  48. data/examples/oauth2/generic_oauth2_api.rb +226 -0
  49. data/examples/oauth2/get_token.rb +72 -0
  50. data/examples/oauth2/songs_api_with_auth0.rb +320 -0
  51. data/examples/oauth2/test_api.sh +16 -0
  52. data/examples/oauth2/test_songs_api.sh +110 -0
  53. data/examples/observability/.env.example +35 -0
  54. data/examples/observability/README.md +230 -0
  55. data/examples/observability/README_HONEYCOMB.md +332 -0
  56. data/examples/observability/advanced_setup.rb +384 -0
  57. data/examples/observability/basic_setup.rb +192 -0
  58. data/examples/observability/complete_test.rb +121 -0
  59. data/examples/observability/honeycomb_example.rb +523 -0
  60. data/examples/observability/honeycomb_rapitapir_clean.rb +488 -0
  61. data/examples/observability/honeycomb_rapitapir_example.rb +523 -0
  62. data/examples/observability/honeycomb_working_example.rb +489 -0
  63. data/examples/observability/quick_test.rb +78 -0
  64. data/examples/observability/simple_test.rb +14 -0
  65. data/examples/observability/test_honeycomb_demo.rb +354 -0
  66. data/examples/observability/test_live_honeycomb.rb +111 -0
  67. data/examples/observability/test_validation.rb +78 -0
  68. data/examples/observability/test_working_validation.rb +66 -0
  69. data/examples/openapi/user_api_schema.rb +132 -0
  70. data/examples/production_ready_example.rb +105 -0
  71. data/examples/rails/users_controller.rb +146 -0
  72. data/examples/readme/basic_sinatra_example.rb +128 -0
  73. data/examples/server/user_api.rb +179 -0
  74. data/examples/simple_auto_derivation_demo.rb +44 -0
  75. data/examples/simple_demo_api.rb +18 -0
  76. data/examples/sinatra/user_app.rb +127 -0
  77. data/examples/t_shortcut_demo.rb +59 -0
  78. data/examples/user_api.rb +190 -0
  79. data/examples/working_getting_started.rb +184 -0
  80. data/examples/working_simple_example.rb +195 -0
  81. data/lib/rapitapir/auth/configuration.rb +129 -0
  82. data/lib/rapitapir/auth/context.rb +122 -0
  83. data/lib/rapitapir/auth/errors.rb +104 -0
  84. data/lib/rapitapir/auth/middleware.rb +324 -0
  85. data/lib/rapitapir/auth/oauth2.rb +350 -0
  86. data/lib/rapitapir/auth/schemes.rb +420 -0
  87. data/lib/rapitapir/auth.rb +113 -0
  88. data/lib/rapitapir/cli/command.rb +535 -0
  89. data/lib/rapitapir/cli/server.rb +243 -0
  90. data/lib/rapitapir/cli/validator.rb +373 -0
  91. data/lib/rapitapir/client/generator_base.rb +272 -0
  92. data/lib/rapitapir/client/typescript_generator.rb +350 -0
  93. data/lib/rapitapir/core/endpoint.rb +158 -0
  94. data/lib/rapitapir/core/enhanced_endpoint.rb +235 -0
  95. data/lib/rapitapir/core/input.rb +182 -0
  96. data/lib/rapitapir/core/output.rb +164 -0
  97. data/lib/rapitapir/core/request.rb +19 -0
  98. data/lib/rapitapir/core/response.rb +17 -0
  99. data/lib/rapitapir/docs/html_generator.rb +780 -0
  100. data/lib/rapitapir/docs/markdown_generator.rb +464 -0
  101. data/lib/rapitapir/dsl/endpoint_dsl.rb +116 -0
  102. data/lib/rapitapir/dsl/enhanced_endpoint_dsl.rb +62 -0
  103. data/lib/rapitapir/dsl/enhanced_input.rb +73 -0
  104. data/lib/rapitapir/dsl/enhanced_output.rb +63 -0
  105. data/lib/rapitapir/dsl/enhanced_structures.rb +393 -0
  106. data/lib/rapitapir/dsl/fluent_dsl.rb +72 -0
  107. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +316 -0
  108. data/lib/rapitapir/dsl/http_verbs.rb +77 -0
  109. data/lib/rapitapir/dsl/input_methods.rb +47 -0
  110. data/lib/rapitapir/dsl/observability_methods.rb +81 -0
  111. data/lib/rapitapir/dsl/output_methods.rb +43 -0
  112. data/lib/rapitapir/dsl/type_resolution.rb +43 -0
  113. data/lib/rapitapir/observability/configuration.rb +108 -0
  114. data/lib/rapitapir/observability/health_check.rb +236 -0
  115. data/lib/rapitapir/observability/logging.rb +270 -0
  116. data/lib/rapitapir/observability/metrics.rb +203 -0
  117. data/lib/rapitapir/observability/middleware.rb +243 -0
  118. data/lib/rapitapir/observability/tracing.rb +143 -0
  119. data/lib/rapitapir/observability.rb +28 -0
  120. data/lib/rapitapir/openapi/schema_generator.rb +403 -0
  121. data/lib/rapitapir/schema.rb +136 -0
  122. data/lib/rapitapir/server/enhanced_rack_adapter.rb +379 -0
  123. data/lib/rapitapir/server/middleware.rb +120 -0
  124. data/lib/rapitapir/server/path_matcher.rb +45 -0
  125. data/lib/rapitapir/server/rack_adapter.rb +215 -0
  126. data/lib/rapitapir/server/rails_adapter.rb +17 -0
  127. data/lib/rapitapir/server/rails_adapter_class.rb +53 -0
  128. data/lib/rapitapir/server/rails_controller.rb +72 -0
  129. data/lib/rapitapir/server/rails_input_processor.rb +73 -0
  130. data/lib/rapitapir/server/rails_response_handler.rb +29 -0
  131. data/lib/rapitapir/server/sinatra_adapter.rb +200 -0
  132. data/lib/rapitapir/server/sinatra_integration.rb +93 -0
  133. data/lib/rapitapir/sinatra/configuration.rb +91 -0
  134. data/lib/rapitapir/sinatra/extension.rb +214 -0
  135. data/lib/rapitapir/sinatra/oauth2_helpers.rb +236 -0
  136. data/lib/rapitapir/sinatra/resource_builder.rb +152 -0
  137. data/lib/rapitapir/sinatra/swagger_ui_generator.rb +166 -0
  138. data/lib/rapitapir/sinatra_rapitapir.rb +40 -0
  139. data/lib/rapitapir/types/array.rb +163 -0
  140. data/lib/rapitapir/types/auto_derivation.rb +265 -0
  141. data/lib/rapitapir/types/base.rb +146 -0
  142. data/lib/rapitapir/types/boolean.rb +46 -0
  143. data/lib/rapitapir/types/date.rb +92 -0
  144. data/lib/rapitapir/types/datetime.rb +98 -0
  145. data/lib/rapitapir/types/email.rb +32 -0
  146. data/lib/rapitapir/types/float.rb +134 -0
  147. data/lib/rapitapir/types/hash.rb +161 -0
  148. data/lib/rapitapir/types/integer.rb +143 -0
  149. data/lib/rapitapir/types/object.rb +156 -0
  150. data/lib/rapitapir/types/optional.rb +65 -0
  151. data/lib/rapitapir/types/string.rb +185 -0
  152. data/lib/rapitapir/types/uuid.rb +32 -0
  153. data/lib/rapitapir/types.rb +155 -0
  154. data/lib/rapitapir/version.rb +5 -0
  155. data/lib/rapitapir.rb +173 -0
  156. data/rapitapir.gemspec +66 -0
  157. 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