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,488 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Add local lib to load path for development
4
+ $LOAD_PATH.unshift(File.expand_path('../../lib', __dir__))
5
+
6
+ # Load environment variables from .env file
7
+ begin
8
+ require 'dotenv'
9
+ Dotenv.load(File.join(__dir__, '.env'))
10
+ rescue LoadError
11
+ puts "⚠️ dotenv gem not available. Make sure environment variables are set manually."
12
+ end
13
+
14
+ require 'json'
15
+ require 'securerandom'
16
+ require 'opentelemetry/sdk'
17
+ require 'opentelemetry/exporter/otlp'
18
+ require 'opentelemetry/instrumentation/all'
19
+ require 'opentelemetry/processor/baggage/baggage_span_processor'
20
+ require 'rapitapir'
21
+ require 'rapitapir/sinatra_rapitapir'
22
+
23
+ # Initialize OpenTelemetry with Honeycomb.io configuration
24
+ OpenTelemetry::SDK.configure do |config|
25
+ config.use_all()
26
+ end
27
+
28
+ # Define schemas using RapiTapir Types
29
+ USER_SCHEMA = RapiTapir::Types.hash({
30
+ 'id' => RapiTapir::Types.string(description: 'Unique user ID'),
31
+ 'name' => RapiTapir::Types.string(min_length: 2, max_length: 100),
32
+ 'email' => RapiTapir::Types.email,
33
+ 'age' => RapiTapir::Types.integer(minimum: 18, maximum: 120),
34
+ 'department' => RapiTapir::Types.string(enum: %w[engineering sales marketing support]),
35
+ 'created_at' => RapiTapir::Types.string(description: 'ISO 8601 timestamp')
36
+ })
37
+
38
+ USER_CREATE_SCHEMA = RapiTapir::Types.hash({
39
+ 'name' => RapiTapir::Types.string(min_length: 2, max_length: 100),
40
+ 'email' => RapiTapir::Types.email,
41
+ 'age' => RapiTapir::Types.integer(minimum: 18, maximum: 120),
42
+ 'department' => RapiTapir::Types.string(enum: %w[engineering sales marketing support])
43
+ })
44
+
45
+ USER_UPDATE_SCHEMA = RapiTapir::Types.hash({
46
+ 'name' => RapiTapir::Types.optional(RapiTapir::Types.string(min_length: 2, max_length: 100)),
47
+ 'email' => RapiTapir::Types.optional(RapiTapir::Types.email),
48
+ 'age' => RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 18, maximum: 120)),
49
+ 'department' => RapiTapir::Types.optional(RapiTapir::Types.string(enum: %w[engineering sales marketing support]))
50
+ })
51
+
52
+ HEALTH_SCHEMA = RapiTapir::Types.hash({
53
+ 'status' => RapiTapir::Types.string,
54
+ 'timestamp' => RapiTapir::Types.string,
55
+ 'service' => RapiTapir::Types.string,
56
+ 'version' => RapiTapir::Types.string,
57
+ 'checks' => RapiTapir::Types.hash({
58
+ 'database' => RapiTapir::Types.hash({
59
+ 'status' => RapiTapir::Types.string,
60
+ 'response_time_ms' => RapiTapir::Types.float
61
+ }),
62
+ 'redis' => RapiTapir::Types.hash({
63
+ 'status' => RapiTapir::Types.string,
64
+ 'response_time_ms' => RapiTapir::Types.float
65
+ })
66
+ })
67
+ })
68
+
69
+ USERS_LIST_SCHEMA = RapiTapir::Types.hash({
70
+ 'users' => RapiTapir::Types.array(USER_SCHEMA),
71
+ 'pagination' => RapiTapir::Types.hash({
72
+ 'page' => RapiTapir::Types.integer,
73
+ 'limit' => RapiTapir::Types.integer,
74
+ 'total' => RapiTapir::Types.integer,
75
+ 'has_more' => RapiTapir::Types.boolean
76
+ })
77
+ })
78
+
79
+ ANALYTICS_SCHEMA = RapiTapir::Types.hash({
80
+ 'timestamp' => RapiTapir::Types.string,
81
+ 'total_users' => RapiTapir::Types.integer,
82
+ 'department_stats' => RapiTapir::Types.array(
83
+ RapiTapir::Types.hash({
84
+ 'department' => RapiTapir::Types.string,
85
+ 'user_count' => RapiTapir::Types.integer,
86
+ 'average_age' => RapiTapir::Types.float
87
+ })
88
+ )
89
+ })
90
+
91
+ # Main API class using SinatraRapiTapir
92
+ class HoneycombDemoAPI < RapiTapir::SinatraRapiTapir
93
+ # In-memory data store for demo
94
+ @@users = []
95
+ @@user_counter = 0
96
+
97
+ # Configure RapiTapir
98
+ rapitapir do
99
+ info(
100
+ title: 'RapiTapir Demo API with Honeycomb.io',
101
+ description: 'Demonstrating observability integration with OpenTelemetry and Honeycomb',
102
+ version: '1.0.0',
103
+ contact: { email: 'demo@rapitapir.dev' }
104
+ )
105
+
106
+ development_defaults!
107
+ enable_docs(path: '/docs', openapi_path: '/openapi.json')
108
+ end
109
+
110
+ # Health Check Endpoint
111
+ endpoint(
112
+ GET('/health')
113
+ .summary('Health check')
114
+ .description('Returns the health status of the API with system checks')
115
+ .tags('Health')
116
+ .ok(HEALTH_SCHEMA)
117
+ .build
118
+ ) do |inputs|
119
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
120
+ tracer.in_span('health_check', attributes: { 'health_check.type' => 'basic' }) do |span|
121
+ health_data = {
122
+ status: 'healthy',
123
+ timestamp: Time.now.iso8601,
124
+ service: 'rapitapir-demo',
125
+ version: '1.0.0',
126
+ checks: {
127
+ database: { status: 'healthy', response_time_ms: 5.2 },
128
+ redis: { status: 'healthy', response_time_ms: 2.1 }
129
+ }
130
+ }
131
+
132
+ span.set_attribute('business.operation', 'health_check')
133
+ span.set_attribute('business.entity', 'system')
134
+ span.set_attribute('health_check.status', 'healthy')
135
+ span.set_attribute('health_check.checks_count', health_data[:checks].size)
136
+
137
+ health_data
138
+ end
139
+ end
140
+
141
+ # List Users Endpoint
142
+ endpoint(
143
+ GET('/users')
144
+ .summary('List users')
145
+ .description('Get a paginated list of users with optional department filtering')
146
+ .tags('Users')
147
+ .query(:page, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1)), description: 'Page number')
148
+ .query(:limit, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1, maximum: 100)), description: 'Items per page')
149
+ .query(:department, RapiTapir::Types.optional(RapiTapir::Types.string(enum: %w[engineering sales marketing support])), description: 'Filter by department')
150
+ .ok(USERS_LIST_SCHEMA)
151
+ .build
152
+ ) do |inputs|
153
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
154
+ tracer.in_span('users.list') do |span|
155
+ page = inputs[:page] || 1
156
+ limit = inputs[:limit] || 10
157
+ department = inputs[:department]
158
+
159
+ span.set_attribute('business.operation', 'list_users')
160
+ span.set_attribute('business.entity', 'user')
161
+ span.set_attribute('pagination.page', page)
162
+ span.set_attribute('pagination.limit', limit)
163
+ span.set_attribute('filter.department', department) if department
164
+
165
+ # Simulate database query
166
+ tracer.in_span('database.select.users') do |db_span|
167
+ db_span.set_attribute('db.operation', 'SELECT')
168
+ db_span.set_attribute('db.table', 'users')
169
+ sleep(0.02)
170
+ end
171
+
172
+ filtered_users = @@users.dup
173
+ filtered_users = filtered_users.select { |u| u[:department] == department } if department
174
+
175
+ # Pagination
176
+ start_index = (page - 1) * limit
177
+ paginated_users = filtered_users[start_index, limit] || []
178
+
179
+ span.set_attribute('result.count', paginated_users.length)
180
+ span.set_attribute('result.total_available', filtered_users.length)
181
+
182
+ {
183
+ users: paginated_users,
184
+ pagination: {
185
+ page: page,
186
+ limit: limit,
187
+ total: filtered_users.length,
188
+ has_more: start_index + limit < filtered_users.length
189
+ }
190
+ }
191
+ end
192
+ end
193
+
194
+ # Create User Endpoint
195
+ endpoint(
196
+ POST('/users')
197
+ .summary('Create user')
198
+ .description('Create a new user with validation')
199
+ .tags('Users')
200
+ .json_body(USER_CREATE_SCHEMA)
201
+ .created(USER_SCHEMA)
202
+ .bad_request(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string, 'details' => RapiTapir::Types.array(RapiTapir::Types.string) }))
203
+ .build
204
+ ) do |inputs|
205
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
206
+ tracer.in_span('users.create') do |span|
207
+ user_data = inputs[:body]
208
+
209
+ span.set_attribute('business.operation', 'create_user')
210
+ span.set_attribute('business.entity', 'user')
211
+ span.set_attribute('input.department', user_data['department'])
212
+
213
+ # Validate input
214
+ validation_errors = tracer.in_span('validation.user_input') do |validation_span|
215
+ errors = []
216
+ existing_email = @@users.find { |u| u[:email] == user_data['email'] }
217
+ errors << 'Email already exists' if existing_email
218
+
219
+ validation_span.set_attribute('validation.errors_count', errors.length)
220
+ validation_span.set_attribute('validation.passed', errors.empty?)
221
+
222
+ errors
223
+ end
224
+
225
+ unless validation_errors.empty?
226
+ span.set_attribute('error.type', 'validation_error')
227
+ span.set_attribute('error.details', validation_errors.join(', '))
228
+ span.status = OpenTelemetry::Trace::Status.error('Validation failed')
229
+
230
+ content_type :json
231
+ status 400
232
+ return { error: 'Validation failed', details: validation_errors }
233
+ end
234
+
235
+ # Create user
236
+ new_user = tracer.in_span('database.insert.user') do |db_span|
237
+ db_span.set_attribute('db.operation', 'INSERT')
238
+ db_span.set_attribute('db.table', 'users')
239
+ sleep(0.015)
240
+
241
+ @@user_counter += 1
242
+ user = {
243
+ id: SecureRandom.uuid,
244
+ name: user_data['name'],
245
+ email: user_data['email'],
246
+ age: user_data['age'],
247
+ department: user_data['department'],
248
+ created_at: Time.now.iso8601
249
+ }
250
+
251
+ @@users << user
252
+ user
253
+ end
254
+
255
+ span.set_attribute('result.user_id', new_user[:id])
256
+ span.set_attribute('result.department', new_user[:department])
257
+
258
+ # Add success baggage
259
+ OpenTelemetry::Baggage.set_value('user.created', 'true')
260
+ OpenTelemetry::Baggage.set_value('user.id', new_user[:id])
261
+
262
+ new_user
263
+ end
264
+ end
265
+
266
+ # Get User by ID Endpoint
267
+ endpoint(
268
+ GET('/users/{id}')
269
+ .summary('Get user')
270
+ .description('Get a specific user by ID')
271
+ .tags('Users')
272
+ .path_param(:id, RapiTapir::Types.string, description: 'User ID')
273
+ .ok(USER_SCHEMA)
274
+ .not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
275
+ .build
276
+ ) do |inputs|
277
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
278
+ tracer.in_span('users.get') do |span|
279
+ user_id = inputs[:id]
280
+
281
+ span.set_attribute('business.operation', 'get_user')
282
+ span.set_attribute('business.entity', 'user')
283
+ span.set_attribute('user.id', user_id)
284
+
285
+ # Find user
286
+ user = tracer.in_span('database.select.user', attributes: { 'db.operation' => 'SELECT', 'db.table' => 'users' }) do
287
+ sleep(0.01)
288
+ @@users.find { |u| u[:id] == user_id }
289
+ end
290
+
291
+ if user
292
+ span.set_attribute('result.found', true)
293
+ span.set_attribute('result.department', user[:department])
294
+ user
295
+ else
296
+ span.set_attribute('result.found', false)
297
+ span.set_attribute('error.type', 'not_found')
298
+ return [404, { 'Content-Type' => 'application/json' }, [{ error: 'User not found' }.to_json]]
299
+ end
300
+ end
301
+ end
302
+
303
+ # Update User Endpoint
304
+ endpoint(
305
+ PUT('/users/{id}')
306
+ .summary('Update user')
307
+ .description('Update an existing user')
308
+ .tags('Users')
309
+ .path_param(:id, RapiTapir::Types.string, description: 'User ID')
310
+ .json_body(USER_UPDATE_SCHEMA)
311
+ .ok(USER_SCHEMA)
312
+ .not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
313
+ .build
314
+ ) do |inputs|
315
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
316
+ tracer.in_span('users.update') do |span|
317
+ user_id = inputs[:id]
318
+ update_data = inputs[:body]
319
+
320
+ span.set_attribute('business.operation', 'update_user')
321
+ span.set_attribute('business.entity', 'user')
322
+ span.set_attribute('user.id', user_id)
323
+
324
+ # Find user index
325
+ user_index = @@users.find_index { |u| u[:id] == user_id }
326
+
327
+ unless user_index
328
+ span.set_attribute('error.type', 'not_found')
329
+ return [404, { 'Content-Type' => 'application/json' }, [{ error: 'User not found' }.to_json]]
330
+ end
331
+
332
+ # Update user
333
+ updated_user = tracer.in_span('database.update.user') do |db_span|
334
+ db_span.set_attribute('db.operation', 'UPDATE')
335
+ db_span.set_attribute('db.table', 'users')
336
+ sleep(0.012)
337
+
338
+ user = @@users[user_index]
339
+ user[:name] = update_data['name'] if update_data['name']
340
+ user[:email] = update_data['email'] if update_data['email']
341
+ user[:age] = update_data['age'] if update_data['age']
342
+ user[:department] = update_data['department'] if update_data['department']
343
+
344
+ @@users[user_index] = user
345
+ user
346
+ end
347
+
348
+ span.set_attribute('result.updated', true)
349
+ span.set_attribute('result.department', updated_user[:department])
350
+
351
+ updated_user
352
+ end
353
+ end
354
+
355
+ # Delete User Endpoint
356
+ endpoint(
357
+ DELETE('/users/{id}')
358
+ .summary('Delete user')
359
+ .description('Delete a user by ID')
360
+ .tags('Users')
361
+ .path_param(:id, RapiTapir::Types.string, description: 'User ID')
362
+ .no_content
363
+ .not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
364
+ .build
365
+ ) do |inputs|
366
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
367
+ tracer.in_span('users.delete') do |span|
368
+ user_id = inputs[:id]
369
+
370
+ span.set_attribute('business.operation', 'delete_user')
371
+ span.set_attribute('business.entity', 'user')
372
+ span.set_attribute('user.id', user_id)
373
+
374
+ # Find user index
375
+ user_index = @@users.find_index { |u| u[:id] == user_id }
376
+
377
+ unless user_index
378
+ span.set_attribute('error.type', 'not_found')
379
+ return [404, { 'Content-Type' => 'application/json' }, [{ error: 'User not found' }.to_json]]
380
+ end
381
+
382
+ # Delete user
383
+ deleted_user = tracer.in_span('database.delete.user') do |db_span|
384
+ db_span.set_attribute('db.operation', 'DELETE')
385
+ db_span.set_attribute('db.table', 'users')
386
+ sleep(0.008)
387
+
388
+ @@users.delete_at(user_index)
389
+ end
390
+
391
+ span.set_attribute('result.deleted', true)
392
+ span.set_attribute('result.department', deleted_user[:department])
393
+
394
+ nil # 204 No Content
395
+ end
396
+ end
397
+
398
+ # Analytics Endpoint
399
+ endpoint(
400
+ GET('/analytics/department-stats')
401
+ .summary('Department analytics')
402
+ .description('Get analytics data grouped by department')
403
+ .tags('Analytics')
404
+ .ok(ANALYTICS_SCHEMA)
405
+ .build
406
+ ) do |inputs|
407
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
408
+ tracer.in_span('analytics.department_stats') do |span|
409
+ span.set_attribute('business.operation', 'department_analytics')
410
+ span.set_attribute('business.entity', 'analytics')
411
+
412
+ # Complex analytics computation
413
+ stats = tracer.in_span('analytics.compute.department_distribution') do |compute_span|
414
+ sleep(0.05)
415
+
416
+ departments = %w[engineering sales marketing support]
417
+ department_stats = departments.map do |dept|
418
+ count = @@users.count { |u| u[:department] == dept }
419
+ avg_age = if count > 0
420
+ ages = @@users.select { |u| u[:department] == dept }.map { |u| u[:age] }
421
+ ages.sum.to_f / ages.length
422
+ else
423
+ 0
424
+ end
425
+
426
+ {
427
+ department: dept,
428
+ user_count: count,
429
+ average_age: avg_age.round(1)
430
+ }
431
+ end
432
+
433
+ compute_span.set_attribute('analytics.departments_analyzed', departments.length)
434
+ compute_span.set_attribute('analytics.total_users', @@users.length)
435
+
436
+ department_stats
437
+ end
438
+
439
+ span.set_attribute('result.departments_count', stats.length)
440
+ span.set_attribute('result.total_users', @@users.length)
441
+
442
+ {
443
+ timestamp: Time.now.iso8601,
444
+ total_users: @@users.length,
445
+ department_stats: stats
446
+ }
447
+ end
448
+ end
449
+
450
+ # Error handling with tracing
451
+ error do
452
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
453
+ tracer.in_span('error.handler') do |span|
454
+ error = env['sinatra.error']
455
+ span.set_attribute('error.type', error.class.name)
456
+ span.set_attribute('error.message', error.message)
457
+ span.status = OpenTelemetry::Trace::Status.error(error.message)
458
+
459
+ content_type :json
460
+ { error: 'Internal server error', message: error.message }.to_json
461
+ end
462
+ end
463
+ end
464
+
465
+ # Configure and run the application
466
+ if __FILE__ == $PROGRAM_NAME
467
+ puts "🍯 Starting RapiTapir Demo API with Honeycomb.io Observability"
468
+ puts "📊 Traces will be sent to: #{ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] || 'https://api.honeycomb.io'}"
469
+ puts "🔧 Service Name: #{ENV['OTEL_SERVICE_NAME'] || 'rapitapir-demo'}"
470
+ puts ""
471
+ puts "🚀 Available endpoints:"
472
+ puts " GET /health - Health check"
473
+ puts " GET /users - List users (supports ?page=1&limit=10&department=engineering)"
474
+ puts " POST /users - Create user"
475
+ puts " GET /users/{id} - Get user by ID"
476
+ puts " PUT /users/{id} - Update user"
477
+ puts " DELETE /users/{id} - Delete user"
478
+ puts " GET /analytics/department-stats - Department analytics"
479
+ puts " GET /docs - Swagger UI documentation"
480
+ puts " GET /openapi.json - OpenAPI specification"
481
+ puts ""
482
+ puts "📝 Example curl commands:"
483
+ puts " curl http://localhost:4567/users"
484
+ puts " curl -X POST http://localhost:4567/users -H 'Content-Type: application/json' -d '{\"name\":\"John Doe\",\"email\":\"john@example.com\",\"age\":30,\"department\":\"engineering\"}'"
485
+ puts ""
486
+
487
+ HoneycombDemoAPI.run!(host: '0.0.0.0', port: 4567)
488
+ end