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,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
|