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,523 @@
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
+ # Observability helper methods
98
+ def tracer
99
+ @tracer ||= OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
100
+ end
101
+
102
+ def with_span(name, **attributes)
103
+ tracer.in_span(name) do |span|
104
+ attributes.each { |key, value| span.set_attribute(key.to_s, value) }
105
+ yield(span) if block_given?
106
+ end
107
+ end
108
+
109
+ def add_business_context(span, operation:, entity:, **attrs)
110
+ span.set_attribute('business.operation', operation)
111
+ span.set_attribute('business.entity', entity)
112
+ attrs.each { |key, value| span.set_attribute(key.to_s, value) }
113
+ end
114
+
115
+ def simulate_db_operation(operation, table, duration = 0.015)
116
+ with_span("database.#{operation}.#{table}",
117
+ 'db.operation' => operation.upcase,
118
+ 'db.table' => table) do |span|
119
+ sleep(duration)
120
+ yield if block_given?
121
+ end
122
+ end
123
+
124
+ # Configure RapiTapir
125
+ rapitapir do
126
+ info(
127
+ title: 'RapiTapir Demo API with Honeycomb.io',
128
+ description: 'Demonstrating observability integration with OpenTelemetry and Honeycomb',
129
+ version: '1.0.0',
130
+ contact: { email: 'demo@rapitapir.dev' }
131
+ )
132
+
133
+ development_defaults!
134
+
135
+ # Enable automatic documentation
136
+ enable_docs(path: '/docs', openapi_path: '/openapi.json')
137
+ end
138
+
139
+ # Health Check Endpoint
140
+ endpoint(
141
+ GET('/health')
142
+ .summary('Health check')
143
+ .description('Returns the health status of the API with system checks')
144
+ .tags('Health')
145
+ .ok(HEALTH_SCHEMA)
146
+ .build
147
+ ) do |inputs|
148
+ with_span('health_check', 'health_check.type' => 'basic') do |span|
149
+ health_data = {
150
+ status: 'healthy',
151
+ timestamp: Time.now.iso8601,
152
+ service: 'rapitapir-demo',
153
+ version: '1.0.0',
154
+ checks: {
155
+ database: { status: 'healthy', response_time_ms: 5.2 },
156
+ redis: { status: 'healthy', response_time_ms: 2.1 }
157
+ }
158
+ }
159
+
160
+ add_business_context(span,
161
+ operation: 'health_check',
162
+ entity: 'system',
163
+ 'health_check.status' => 'healthy',
164
+ 'health_check.checks_count' => health_data[:checks].size)
165
+
166
+ health_data
167
+ end
168
+ end
169
+
170
+ # List Users Endpoint
171
+ endpoint(
172
+ GET('/users')
173
+ .summary('List users')
174
+ .description('Get a paginated list of users with optional department filtering')
175
+ .tags('Users')
176
+ .query(:page, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1)), description: 'Page number')
177
+ .query(:limit, RapiTapir::Types.optional(RapiTapir::Types.integer(minimum: 1, maximum: 100)), description: 'Items per page')
178
+ .query(:department, RapiTapir::Types.optional(RapiTapir::Types.string(enum: %w[engineering sales marketing support])), description: 'Filter by department')
179
+ .ok(USERS_LIST_SCHEMA)
180
+ .build
181
+ ) do |inputs|
182
+ tracer = OpenTelemetry.tracer_provider.tracer('rapitapir-business-logic')
183
+ tracer.in_span('users.list') do |span|
184
+ page = inputs[:page] || 1
185
+ limit = inputs[:limit] || 10
186
+ department = inputs[:department]
187
+
188
+ add_business_context(span,
189
+ operation: 'list_users',
190
+ entity: 'user',
191
+ 'pagination.page' => page,
192
+ 'pagination.limit' => limit)
193
+
194
+ span.set_attribute('filter.department', department) if department
195
+
196
+ # Simulate database query
197
+ filtered_users = simulate_db_operation('select', 'users', 0.02) do
198
+ users = @@users.dup
199
+ users = users.select { |u| u[:department] == department } if department
200
+ users
201
+ end
202
+
203
+ # Pagination
204
+ start_index = (page - 1) * limit
205
+ paginated_users = filtered_users[start_index, limit] || []
206
+
207
+ add_business_context(span,
208
+ operation: 'list_users',
209
+ entity: 'user',
210
+ 'result.count' => paginated_users.length,
211
+ 'result.total_available' => filtered_users.length)
212
+
213
+ {
214
+ users: paginated_users,
215
+ pagination: {
216
+ page: page,
217
+ limit: limit,
218
+ total: filtered_users.length,
219
+ has_more: start_index + limit < filtered_users.length
220
+ }
221
+ }
222
+ end
223
+ end
224
+
225
+ # Create User Endpoint
226
+ endpoint(
227
+ POST('/users')
228
+ .summary('Create user')
229
+ .description('Create a new user with validation')
230
+ .tags('Users')
231
+ .json_body(USER_CREATE_SCHEMA)
232
+ .created(USER_SCHEMA)
233
+ .bad_request(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string, 'details' => RapiTapir::Types.array(RapiTapir::Types.string) }))
234
+ .build
235
+ ) do |inputs|
236
+ with_span('users.create') do |span|
237
+ user_data = inputs[:body]
238
+
239
+ add_business_context(span,
240
+ operation: 'create_user',
241
+ entity: 'user',
242
+ 'input.department' => user_data['department'])
243
+
244
+ # Validate input with detailed tracing
245
+ validation_errors = with_span('validation.user_input') do |validation_span|
246
+ errors = []
247
+
248
+ # Custom business validation beyond schema
249
+ existing_email = @@users.find { |u| u[:email] == user_data['email'] }
250
+ errors << 'Email already exists' if existing_email
251
+
252
+ validation_span.set_attribute('validation.errors_count', errors.length)
253
+ validation_span.set_attribute('validation.passed', errors.empty?)
254
+
255
+ errors
256
+ end
257
+
258
+ unless validation_errors.empty?
259
+ span.set_attribute('error.type', 'validation_error')
260
+ span.set_attribute('error.details', validation_errors.join(', '))
261
+ span.status = OpenTelemetry::Trace::Status.error('Validation failed')
262
+
263
+ halt 400, { error: 'Validation failed', details: validation_errors }.to_json
264
+ end
265
+
266
+ # Create user with database simulation
267
+ new_user = simulate_db_operation('insert', 'user', 0.015) do
268
+ @@user_counter += 1
269
+ user = {
270
+ id: SecureRandom.uuid,
271
+ name: user_data['name'],
272
+ email: user_data['email'],
273
+ age: user_data['age'],
274
+ department: user_data['department'],
275
+ created_at: Time.now.iso8601
276
+ }
277
+
278
+ @@users << user
279
+ user
280
+ end
281
+
282
+ add_business_context(span,
283
+ operation: 'create_user',
284
+ entity: 'user',
285
+ 'result.user_id' => new_user[:id],
286
+ 'result.department' => new_user[:department])
287
+
288
+ # Add success baggage for downstream spans
289
+ OpenTelemetry::Baggage.set_value('user.created', 'true')
290
+ OpenTelemetry::Baggage.set_value('user.id', new_user[:id])
291
+
292
+ status 201
293
+ new_user
294
+ end
295
+ end
296
+
297
+ # Get User by ID Endpoint
298
+ endpoint(
299
+ GET('/users/{id}')
300
+ .summary('Get user')
301
+ .description('Get a specific user by ID')
302
+ .tags('Users')
303
+ .path_param(:id, RapiTapir::Types.string, description: 'User ID')
304
+ .ok(USER_SCHEMA)
305
+ .not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
306
+ .build
307
+ ) do |inputs|
308
+ with_span('users.get') do |span|
309
+ user_id = inputs[:id]
310
+
311
+ add_business_context(span,
312
+ operation: 'get_user',
313
+ entity: 'user',
314
+ 'user.id' => user_id)
315
+
316
+ # Find user with database simulation
317
+ user = simulate_db_operation('select', 'user', 0.01) do
318
+ @@users.find { |u| u[:id] == user_id }
319
+ end
320
+
321
+ if user
322
+ add_business_context(span,
323
+ operation: 'get_user',
324
+ entity: 'user',
325
+ 'result.found' => true,
326
+ 'result.department' => user[:department])
327
+ user
328
+ else
329
+ span.set_attribute('result.found', false)
330
+ span.set_attribute('error.type', 'not_found')
331
+ halt 404, { error: 'User not found' }.to_json
332
+ end
333
+ end
334
+ end
335
+
336
+ # Update User Endpoint
337
+ endpoint(
338
+ PUT('/users/{id}')
339
+ .summary('Update user')
340
+ .description('Update an existing user')
341
+ .tags('Users')
342
+ .path_param(:id, RapiTapir::Types.string, description: 'User ID')
343
+ .json_body(USER_UPDATE_SCHEMA)
344
+ .ok(USER_SCHEMA)
345
+ .not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
346
+ .build
347
+ ) do |inputs|
348
+ with_span('users.update') do |span|
349
+ user_id = inputs[:id]
350
+ update_data = inputs[:body]
351
+
352
+ add_business_context(span,
353
+ operation: 'update_user',
354
+ entity: 'user',
355
+ 'user.id' => user_id)
356
+
357
+ # Find user index
358
+ user_index = @@users.find_index { |u| u[:id] == user_id }
359
+
360
+ unless user_index
361
+ span.set_attribute('error.type', 'not_found')
362
+ halt 404, { error: 'User not found' }.to_json
363
+ end
364
+
365
+ # Update with database simulation
366
+ updated_user = simulate_db_operation('update', 'user', 0.012) do
367
+ user = @@users[user_index]
368
+ user[:name] = update_data['name'] if update_data['name']
369
+ user[:email] = update_data['email'] if update_data['email']
370
+ user[:age] = update_data['age'] if update_data['age']
371
+ user[:department] = update_data['department'] if update_data['department']
372
+
373
+ @@users[user_index] = user
374
+ user
375
+ end
376
+
377
+ add_business_context(span,
378
+ operation: 'update_user',
379
+ entity: 'user',
380
+ 'result.updated' => true,
381
+ 'result.department' => updated_user[:department])
382
+
383
+ updated_user
384
+ end
385
+ end
386
+
387
+ # Delete User Endpoint
388
+ endpoint(
389
+ DELETE('/users/{id}')
390
+ .summary('Delete user')
391
+ .description('Delete a user by ID')
392
+ .tags('Users')
393
+ .path_param(:id, RapiTapir::Types.string, description: 'User ID')
394
+ .no_content
395
+ .not_found(RapiTapir::Types.hash({ 'error' => RapiTapir::Types.string }))
396
+ .build
397
+ ) do |inputs|
398
+ with_span('users.delete') do |span|
399
+ user_id = inputs[:id]
400
+
401
+ add_business_context(span,
402
+ operation: 'delete_user',
403
+ entity: 'user',
404
+ 'user.id' => user_id)
405
+
406
+ # Find user index
407
+ user_index = @@users.find_index { |u| u[:id] == user_id }
408
+
409
+ unless user_index
410
+ span.set_attribute('error.type', 'not_found')
411
+ halt 404, { error: 'User not found' }.to_json
412
+ end
413
+
414
+ # Delete with database simulation
415
+ deleted_user = simulate_db_operation('delete', 'user', 0.008) do
416
+ @@users.delete_at(user_index)
417
+ end
418
+
419
+ add_business_context(span,
420
+ operation: 'delete_user',
421
+ entity: 'user',
422
+ 'result.deleted' => true,
423
+ 'result.department' => deleted_user[:department])
424
+
425
+ status 204
426
+ ''
427
+ end
428
+ end
429
+
430
+ # Analytics Endpoint
431
+ endpoint(
432
+ GET('/analytics/department-stats')
433
+ .summary('Department analytics')
434
+ .description('Get analytics data grouped by department')
435
+ .tags('Analytics')
436
+ .ok(ANALYTICS_SCHEMA)
437
+ .build
438
+ ) do |inputs|
439
+ with_span('analytics.department_stats') do |span|
440
+ add_business_context(span,
441
+ operation: 'department_analytics',
442
+ entity: 'analytics')
443
+
444
+ # Complex analytics with child spans
445
+ stats = with_span('analytics.compute.department_distribution') do |compute_span|
446
+ # Simulate complex computation
447
+ sleep(0.05)
448
+
449
+ departments = %w[engineering sales marketing support]
450
+ department_stats = departments.map do |dept|
451
+ count = @@users.count { |u| u[:department] == dept }
452
+ avg_age = if count > 0
453
+ ages = @@users.select { |u| u[:department] == dept }.map { |u| u[:age] }
454
+ ages.sum.to_f / ages.length
455
+ else
456
+ 0
457
+ end
458
+
459
+ {
460
+ department: dept,
461
+ user_count: count,
462
+ average_age: avg_age.round(1)
463
+ }
464
+ end
465
+
466
+ compute_span.set_attribute('analytics.departments_analyzed', departments.length)
467
+ compute_span.set_attribute('analytics.total_users', @@users.length)
468
+
469
+ department_stats
470
+ end
471
+
472
+ add_business_context(span,
473
+ operation: 'department_analytics',
474
+ entity: 'analytics',
475
+ 'result.departments_count' => stats.length,
476
+ 'result.total_users' => @@users.length)
477
+
478
+ {
479
+ timestamp: Time.now.iso8601,
480
+ total_users: @@users.length,
481
+ department_stats: stats
482
+ }
483
+ end
484
+ end
485
+
486
+ # Error handling with tracing
487
+ error do
488
+ tracer.in_span('error.handler') do |span|
489
+ error = env['sinatra.error']
490
+ span.set_attribute('error.type', error.class.name)
491
+ span.set_attribute('error.message', error.message)
492
+ span.status = OpenTelemetry::Trace::Status.error(error.message)
493
+
494
+ content_type :json
495
+ { error: 'Internal server error', message: error.message }.to_json
496
+ end
497
+ end
498
+ end
499
+
500
+ # Configure and run the application
501
+ if __FILE__ == $PROGRAM_NAME
502
+ puts "🍯 Starting RapiTapir Demo API with Honeycomb.io Observability"
503
+ puts "📊 Traces will be sent to: #{ENV['OTEL_EXPORTER_OTLP_ENDPOINT'] || 'https://api.honeycomb.io'}"
504
+ puts "🔧 Service Name: #{ENV['OTEL_SERVICE_NAME'] || 'rapitapir-demo'}"
505
+ puts ""
506
+ puts "🚀 Available endpoints:"
507
+ puts " GET /health - Health check"
508
+ puts " GET /users - List users (supports ?page=1&limit=10&department=engineering)"
509
+ puts " POST /users - Create user"
510
+ puts " GET /users/{id} - Get user by ID"
511
+ puts " PUT /users/{id} - Update user"
512
+ puts " DELETE /users/{id} - Delete user"
513
+ puts " GET /analytics/department-stats - Department analytics"
514
+ puts " GET /docs - Swagger UI documentation"
515
+ puts " GET /openapi.json - OpenAPI specification"
516
+ puts ""
517
+ puts "📝 Example curl commands:"
518
+ puts " curl http://localhost:4567/users"
519
+ 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\"}'"
520
+ puts ""
521
+
522
+ HoneycombDemoAPI.run!(host: '0.0.0.0', port: 4567)
523
+ end