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,93 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'enhanced_rack_adapter'
|
4
|
+
|
5
|
+
module RapiTapir
|
6
|
+
module Server
|
7
|
+
# Sinatra integration for enhanced endpoints
|
8
|
+
module SinatraIntegration
|
9
|
+
def self.included(base)
|
10
|
+
base.extend(ClassMethods)
|
11
|
+
base.class_eval do
|
12
|
+
@rapitapir_adapter = EnhancedRackAdapter.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
# Class methods for Sinatra integration
|
17
|
+
#
|
18
|
+
# Provides methods to define RapiTapir endpoints and generate documentation
|
19
|
+
# within Sinatra applications.
|
20
|
+
module ClassMethods
|
21
|
+
def rapitapir_adapter
|
22
|
+
@rapitapir_adapter ||= EnhancedRackAdapter.new
|
23
|
+
end
|
24
|
+
|
25
|
+
# Mount an endpoint in Sinatra
|
26
|
+
def mount_endpoint(endpoint, &handler)
|
27
|
+
rapitapir_adapter.mount(endpoint, &handler)
|
28
|
+
|
29
|
+
# Register the route with Sinatra
|
30
|
+
method_name = endpoint.method.to_s.downcase
|
31
|
+
path_pattern = convert_path_to_sinatra(endpoint.path)
|
32
|
+
|
33
|
+
send(method_name, path_pattern) do
|
34
|
+
# Delegate to RapiTapir adapter
|
35
|
+
rapitapir_adapter.call(env)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
# Use middleware with RapiTapir
|
40
|
+
def rapitapir_use(middleware_class, ...)
|
41
|
+
rapitapir_adapter.use(middleware_class, ...)
|
42
|
+
end
|
43
|
+
|
44
|
+
# Register error handlers
|
45
|
+
def rapitapir_error(error_class, &handler)
|
46
|
+
rapitapir_adapter.on_error(error_class, &handler)
|
47
|
+
end
|
48
|
+
|
49
|
+
# Generate OpenAPI spec for all mounted endpoints
|
50
|
+
def to_openapi_spec(info = {})
|
51
|
+
spec = build_base_openapi_spec(info)
|
52
|
+
add_endpoints_to_spec(spec)
|
53
|
+
spec
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def build_base_openapi_spec(info)
|
59
|
+
{
|
60
|
+
openapi: '3.0.3',
|
61
|
+
info: build_openapi_info(info),
|
62
|
+
paths: {}
|
63
|
+
}
|
64
|
+
end
|
65
|
+
|
66
|
+
def build_openapi_info(info)
|
67
|
+
{
|
68
|
+
title: info[:title] || 'API',
|
69
|
+
version: info[:version] || '1.0.0',
|
70
|
+
description: info[:description]
|
71
|
+
}.compact
|
72
|
+
end
|
73
|
+
|
74
|
+
def add_endpoints_to_spec(spec)
|
75
|
+
rapitapir_adapter.endpoints.each do |endpoint_data|
|
76
|
+
endpoint = endpoint_data[:endpoint]
|
77
|
+
path = endpoint.path
|
78
|
+
method = endpoint.method.to_s.downcase
|
79
|
+
|
80
|
+
spec[:paths][path] ||= {}
|
81
|
+
spec[:paths][path][method] = endpoint.to_openapi_spec
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
def convert_path_to_sinatra(path)
|
86
|
+
# Convert RapiTapir path format to Sinatra path format
|
87
|
+
# e.g., "/users/{id}" -> "/users/:id"
|
88
|
+
path.gsub(/\{([^}]+)\}/, ':\1')
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Sinatra
|
5
|
+
# Configuration class for RapiTapir Sinatra integration
|
6
|
+
# Follows Single Responsibility Principle - manages configuration only
|
7
|
+
class Configuration
|
8
|
+
attr_accessor :docs_path, :openapi_path, :health_check_enabled, :health_check_path
|
9
|
+
attr_reader :api_info, :servers, :public_paths, :auth_schemes
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@api_info = {
|
13
|
+
title: 'API Documentation',
|
14
|
+
description: 'Generated with RapiTapir',
|
15
|
+
version: '1.0.0'
|
16
|
+
}
|
17
|
+
@servers = []
|
18
|
+
@public_paths = []
|
19
|
+
@auth_schemes = {}
|
20
|
+
@docs_path = '/docs'
|
21
|
+
@openapi_path = '/openapi.json'
|
22
|
+
@health_check_enabled = false
|
23
|
+
@health_check_path = '/health'
|
24
|
+
end
|
25
|
+
|
26
|
+
# API Information configuration
|
27
|
+
def info(title: nil, description: nil, version: nil, **options)
|
28
|
+
@api_info[:title] = title if title
|
29
|
+
@api_info[:description] = description if description
|
30
|
+
@api_info[:version] = version if version
|
31
|
+
@api_info.merge!(options)
|
32
|
+
end
|
33
|
+
|
34
|
+
# Server configuration
|
35
|
+
def server(url:, description: nil)
|
36
|
+
@servers << { url: url, description: description }.compact
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add paths that don't require authentication
|
40
|
+
def add_public_paths(*paths)
|
41
|
+
@public_paths.concat(paths.flatten.map(&:to_s))
|
42
|
+
end
|
43
|
+
|
44
|
+
# Authentication scheme management
|
45
|
+
def add_auth_scheme(name, scheme)
|
46
|
+
@auth_schemes[name] = scheme
|
47
|
+
end
|
48
|
+
|
49
|
+
def get_auth_scheme(name)
|
50
|
+
@auth_schemes[name]
|
51
|
+
end
|
52
|
+
|
53
|
+
# Check if documentation is enabled
|
54
|
+
def docs_enabled?
|
55
|
+
!@docs_path.nil?
|
56
|
+
end
|
57
|
+
|
58
|
+
# Environment-specific configurations
|
59
|
+
def development_defaults!
|
60
|
+
# Enable health check endpoint
|
61
|
+
enable_health_check
|
62
|
+
# Enable docs by default in development
|
63
|
+
enable_docs unless @docs_path.nil? && @openapi_path.nil?
|
64
|
+
# Add health check to public paths (no auth required)
|
65
|
+
add_public_paths(@health_check_path)
|
66
|
+
puts '📝 Applied development defaults for RapiTapir (includes health check at /health)'
|
67
|
+
end
|
68
|
+
|
69
|
+
def production_defaults!
|
70
|
+
# Basic production settings
|
71
|
+
puts '🔒 Applied production defaults for RapiTapir'
|
72
|
+
end
|
73
|
+
|
74
|
+
# Health check configuration
|
75
|
+
def enable_health_check(path: '/health')
|
76
|
+
@health_check_enabled = true
|
77
|
+
@health_check_path = path
|
78
|
+
end
|
79
|
+
|
80
|
+
def health_check_enabled?
|
81
|
+
@health_check_enabled
|
82
|
+
end
|
83
|
+
|
84
|
+
# Documentation configuration
|
85
|
+
def enable_docs(path: '/docs', openapi_path: '/openapi.json')
|
86
|
+
@docs_path = path
|
87
|
+
@openapi_path = openapi_path
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,214 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'sinatra/base'
|
4
|
+
require_relative '../server/sinatra_adapter'
|
5
|
+
require_relative '../auth'
|
6
|
+
require_relative '../openapi/schema_generator'
|
7
|
+
require_relative 'configuration'
|
8
|
+
require_relative 'resource_builder'
|
9
|
+
require_relative 'swagger_ui_generator'
|
10
|
+
require_relative 'oauth2_helpers'
|
11
|
+
require_relative '../dsl/http_verbs'
|
12
|
+
|
13
|
+
module RapiTapir
|
14
|
+
module Sinatra
|
15
|
+
# Main Sinatra Extension for RapiTapir integration
|
16
|
+
# Provides a seamless, ergonomic experience for building enterprise-grade APIs
|
17
|
+
module Extension
|
18
|
+
# Extension registration hook
|
19
|
+
def self.registered(app)
|
20
|
+
app.helpers Helpers
|
21
|
+
app.extend ClassMethods
|
22
|
+
app.extend DSL::HTTPVerbs # Automatically include enhanced HTTP verb DSL
|
23
|
+
app.extend OAuth2Helpers # Include OAuth2 authentication helpers
|
24
|
+
app.set :rapitapir_config, Configuration.new
|
25
|
+
app.set :rapitapir_endpoints, []
|
26
|
+
app.set :rapitapir_adapter, nil
|
27
|
+
end
|
28
|
+
|
29
|
+
# Class methods added to the Sinatra application
|
30
|
+
module ClassMethods
|
31
|
+
# Configure RapiTapir integration
|
32
|
+
def rapitapir(&block)
|
33
|
+
config = settings.rapitapir_config
|
34
|
+
config.instance_eval(&block) if block_given?
|
35
|
+
setup_rapitapir_integration
|
36
|
+
end
|
37
|
+
|
38
|
+
# Register an endpoint with automatic route creation
|
39
|
+
def endpoint(definition, &handler)
|
40
|
+
endpoint_obj = case definition
|
41
|
+
when RapiTapir::Core::Endpoint, RapiTapir::Core::EnhancedEndpoint
|
42
|
+
definition
|
43
|
+
when Proc
|
44
|
+
definition.call
|
45
|
+
else
|
46
|
+
raise ArgumentError, 'Invalid endpoint definition'
|
47
|
+
end
|
48
|
+
|
49
|
+
# Store the endpoint
|
50
|
+
settings.rapitapir_endpoints << { endpoint: endpoint_obj, handler: handler }
|
51
|
+
|
52
|
+
# Register with adapter if available
|
53
|
+
settings.rapitapir_adapter&.register_endpoint(endpoint_obj, handler)
|
54
|
+
|
55
|
+
endpoint_obj
|
56
|
+
end
|
57
|
+
|
58
|
+
# DSL for common endpoint patterns
|
59
|
+
def api_resource(path, schema:, **options, &block)
|
60
|
+
ResourceBuilder.new(self, path, schema, **options).instance_eval(&block)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Authentication configuration
|
64
|
+
def auth_scheme(name, type, **config)
|
65
|
+
settings.rapitapir_config.add_auth_scheme(name, type, **config)
|
66
|
+
end
|
67
|
+
|
68
|
+
# Middleware configuration
|
69
|
+
def use_rapitapir_middleware(*middleware_types)
|
70
|
+
middleware_types.each { |type| settings.rapitapir_config.enable_middleware(type) }
|
71
|
+
end
|
72
|
+
|
73
|
+
# OpenAPI documentation endpoints
|
74
|
+
def enable_docs(path: '/docs', openapi_path: '/openapi.json')
|
75
|
+
settings.rapitapir_config.docs_path = path
|
76
|
+
settings.rapitapir_config.openapi_path = openapi_path
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
def setup_rapitapir_integration
|
82
|
+
return if settings.rapitapir_adapter
|
83
|
+
|
84
|
+
config = settings.rapitapir_config
|
85
|
+
|
86
|
+
# Create and configure adapter
|
87
|
+
adapter = RapiTapir::Server::SinatraAdapter.new(self)
|
88
|
+
set :rapitapir_adapter, adapter
|
89
|
+
|
90
|
+
# Register existing endpoints
|
91
|
+
settings.rapitapir_endpoints.each do |ep_data|
|
92
|
+
adapter.register_endpoint(ep_data[:endpoint], ep_data[:handler])
|
93
|
+
end
|
94
|
+
|
95
|
+
# Setup documentation endpoints
|
96
|
+
setup_documentation_endpoints(config) if config.docs_enabled?
|
97
|
+
|
98
|
+
# Setup health check endpoint automatically
|
99
|
+
setup_health_check_endpoint(config) if config.health_check_enabled?
|
100
|
+
end
|
101
|
+
|
102
|
+
def setup_documentation_endpoints(config)
|
103
|
+
openapi_path = config.openapi_path
|
104
|
+
docs_path = config.docs_path
|
105
|
+
|
106
|
+
# OpenAPI spec endpoint
|
107
|
+
get openapi_path do
|
108
|
+
content_type :json
|
109
|
+
JSON.pretty_generate(generate_openapi_spec)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Swagger UI endpoint
|
113
|
+
get docs_path do
|
114
|
+
generate_swagger_ui(openapi_path, config.api_info)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def setup_health_check_endpoint(config)
|
119
|
+
health_path = config.health_check_path
|
120
|
+
health_endpoint = build_health_check_endpoint(health_path)
|
121
|
+
health_handler = build_health_check_handler(config)
|
122
|
+
|
123
|
+
# Register the health check endpoint
|
124
|
+
settings.rapitapir_endpoints << { endpoint: health_endpoint, handler: health_handler }
|
125
|
+
|
126
|
+
# Register with adapter
|
127
|
+
settings.rapitapir_adapter&.register_endpoint(health_endpoint, health_handler)
|
128
|
+
end
|
129
|
+
|
130
|
+
def build_health_check_endpoint(health_path)
|
131
|
+
RapiTapir.get(health_path)
|
132
|
+
.summary('Health check')
|
133
|
+
.description('Returns the health status of the API')
|
134
|
+
.tags('Health')
|
135
|
+
.ok(build_health_check_schema)
|
136
|
+
.build
|
137
|
+
end
|
138
|
+
|
139
|
+
def build_health_check_schema
|
140
|
+
RapiTapir::Types.hash({
|
141
|
+
'status' => RapiTapir::Types.string,
|
142
|
+
'timestamp' => RapiTapir::Types.string,
|
143
|
+
'version' => RapiTapir::Types.optional(RapiTapir::Types.string)
|
144
|
+
})
|
145
|
+
end
|
146
|
+
|
147
|
+
def build_health_check_handler(config)
|
148
|
+
proc do |_inputs|
|
149
|
+
{
|
150
|
+
status: 'healthy',
|
151
|
+
timestamp: Time.now.iso8601,
|
152
|
+
version: config.api_info[:version]
|
153
|
+
}
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
# Instance methods added to the Sinatra application
|
159
|
+
module Helpers
|
160
|
+
# Generate OpenAPI specification from registered endpoints
|
161
|
+
def generate_openapi_spec
|
162
|
+
config = settings.rapitapir_config
|
163
|
+
endpoints = settings.rapitapir_endpoints.map { |ep| ep[:endpoint] }
|
164
|
+
|
165
|
+
generator = RapiTapir::OpenAPI::SchemaGenerator.new(
|
166
|
+
endpoints: endpoints,
|
167
|
+
info: config.api_info,
|
168
|
+
servers: config.servers
|
169
|
+
)
|
170
|
+
|
171
|
+
generator.generate
|
172
|
+
end
|
173
|
+
|
174
|
+
# Generate Swagger UI HTML
|
175
|
+
def generate_swagger_ui(openapi_path, api_info)
|
176
|
+
SwaggerUIGenerator.new(openapi_path, api_info).generate
|
177
|
+
end
|
178
|
+
|
179
|
+
# Authentication helpers
|
180
|
+
def current_user
|
181
|
+
@current_user
|
182
|
+
end
|
183
|
+
|
184
|
+
def authenticated?
|
185
|
+
!current_user.nil?
|
186
|
+
end
|
187
|
+
|
188
|
+
def scope?(scope)
|
189
|
+
return false unless authenticated?
|
190
|
+
|
191
|
+
user_scopes = current_user.is_a?(Hash) ? current_user[:scopes] || [] : []
|
192
|
+
user_scopes.include?(scope.to_s)
|
193
|
+
end
|
194
|
+
alias has_scope? scope?
|
195
|
+
|
196
|
+
def require_authentication!
|
197
|
+
halt 401, { error: 'Authentication required' }.to_json unless authenticated?
|
198
|
+
end
|
199
|
+
|
200
|
+
def require_scope!(scope)
|
201
|
+
require_authentication!
|
202
|
+
halt 403, { error: "#{scope.capitalize} permission required" }.to_json unless has_scope?(scope)
|
203
|
+
end
|
204
|
+
|
205
|
+
private
|
206
|
+
|
207
|
+
# Simple placeholder for future security scheme building
|
208
|
+
def build_security_schemes(_config)
|
209
|
+
{}
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
213
|
+
end
|
214
|
+
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative '../auth/oauth2'
|
4
|
+
|
5
|
+
module RapiTapir
|
6
|
+
module Sinatra
|
7
|
+
# OAuth2 integration helpers for Sinatra applications
|
8
|
+
# Provides convenient methods to secure endpoints with OAuth2/Auth0
|
9
|
+
module OAuth2Helpers
|
10
|
+
# Configure Auth0 OAuth2 authentication
|
11
|
+
def auth0_oauth2(scheme_name = :oauth2_auth0, domain:, audience:, **options)
|
12
|
+
auth_scheme = RapiTapir::Auth::OAuth2::Auth0Scheme.new(scheme_name, {
|
13
|
+
domain: domain,
|
14
|
+
audience: audience,
|
15
|
+
**options
|
16
|
+
})
|
17
|
+
|
18
|
+
# Store the auth scheme for use in endpoint protection
|
19
|
+
settings.rapitapir_config.add_auth_scheme(scheme_name, auth_scheme)
|
20
|
+
|
21
|
+
# Add authentication helper methods
|
22
|
+
helpers do
|
23
|
+
include OAuth2HelperMethods
|
24
|
+
end
|
25
|
+
|
26
|
+
auth_scheme
|
27
|
+
end
|
28
|
+
|
29
|
+
# Configure generic OAuth2 authentication with token introspection
|
30
|
+
def oauth2_introspection(scheme_name = :oauth2, introspection_endpoint:, client_id:, client_secret:, **options)
|
31
|
+
auth_scheme = RapiTapir::Auth::OAuth2::GenericScheme.new(scheme_name, {
|
32
|
+
introspection_endpoint: introspection_endpoint,
|
33
|
+
client_id: client_id,
|
34
|
+
client_secret: client_secret,
|
35
|
+
**options
|
36
|
+
})
|
37
|
+
|
38
|
+
settings.rapitapir_config.add_auth_scheme(scheme_name, auth_scheme)
|
39
|
+
|
40
|
+
helpers do
|
41
|
+
include OAuth2HelperMethods
|
42
|
+
end
|
43
|
+
|
44
|
+
auth_scheme
|
45
|
+
end
|
46
|
+
|
47
|
+
# Protect specific routes with OAuth2 authentication
|
48
|
+
def protect_with_oauth2(*paths, scopes: [], scheme: :oauth2_auth0)
|
49
|
+
paths.each do |path|
|
50
|
+
before path do
|
51
|
+
authorize_oauth2!(required_scopes: scopes, scheme: scheme)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
# Protect all routes with OAuth2 authentication
|
57
|
+
def protect_all_routes_with_oauth2(scopes: [], scheme: :oauth2_auth0, except: [])
|
58
|
+
before do
|
59
|
+
# Skip protection for excluded paths
|
60
|
+
next if except.any? { |pattern| request.path_info.match?(pattern) }
|
61
|
+
|
62
|
+
authorize_oauth2!(required_scopes: scopes, scheme: scheme)
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Helper methods available in route handlers
|
68
|
+
module OAuth2HelperMethods
|
69
|
+
# Authenticate and authorize with OAuth2
|
70
|
+
def authorize_oauth2!(required_scopes: [], scheme: :oauth2_auth0)
|
71
|
+
auth_scheme = settings.rapitapir_config.auth_schemes[scheme]
|
72
|
+
|
73
|
+
unless auth_scheme
|
74
|
+
halt 500, { error: 'OAuth2 authentication not configured' }.to_json
|
75
|
+
end
|
76
|
+
|
77
|
+
begin
|
78
|
+
context = auth_scheme.authenticate(request)
|
79
|
+
|
80
|
+
unless context
|
81
|
+
challenge = auth_scheme.challenge
|
82
|
+
headers 'WWW-Authenticate' => challenge
|
83
|
+
halt 401, {
|
84
|
+
error: 'unauthorized',
|
85
|
+
error_description: 'Access token required'
|
86
|
+
}.to_json
|
87
|
+
end
|
88
|
+
|
89
|
+
# Store context for use in route handlers
|
90
|
+
request.env['rapitapir.auth.context'] = context
|
91
|
+
|
92
|
+
# Check required scopes if specified
|
93
|
+
if required_scopes.any?
|
94
|
+
missing_scopes = required_scopes - context.scopes
|
95
|
+
|
96
|
+
if missing_scopes.any?
|
97
|
+
halt 403, {
|
98
|
+
error: 'insufficient_scope',
|
99
|
+
error_description: "Missing required scopes: #{missing_scopes.join(', ')}"
|
100
|
+
}.to_json
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
context
|
105
|
+
rescue RapiTapir::Auth::InvalidTokenError => e
|
106
|
+
challenge = auth_scheme.challenge
|
107
|
+
headers 'WWW-Authenticate' => challenge
|
108
|
+
halt 401, {
|
109
|
+
error: 'invalid_token',
|
110
|
+
error_description: e.message
|
111
|
+
}.to_json
|
112
|
+
rescue RapiTapir::Auth::AuthenticationError => e
|
113
|
+
halt 500, {
|
114
|
+
error: 'authentication_failed',
|
115
|
+
error_description: e.message
|
116
|
+
}.to_json
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Get current authentication context
|
121
|
+
def current_auth_context
|
122
|
+
request.env['rapitapir.auth.context']
|
123
|
+
end
|
124
|
+
|
125
|
+
# Get current authenticated user
|
126
|
+
def current_user
|
127
|
+
current_auth_context&.user
|
128
|
+
end
|
129
|
+
|
130
|
+
# Check if user is authenticated
|
131
|
+
def authenticated?
|
132
|
+
!current_auth_context.nil?
|
133
|
+
end
|
134
|
+
|
135
|
+
# Check if user has specific scope
|
136
|
+
def has_scope?(scope)
|
137
|
+
return false unless current_auth_context
|
138
|
+
|
139
|
+
current_auth_context.scopes.include?(scope.to_s)
|
140
|
+
end
|
141
|
+
|
142
|
+
# Check if user has all required scopes
|
143
|
+
def has_scopes?(*scopes)
|
144
|
+
return false unless current_auth_context
|
145
|
+
|
146
|
+
scopes.all? { |scope| has_scope?(scope) }
|
147
|
+
end
|
148
|
+
|
149
|
+
# Check if user has any of the specified scopes
|
150
|
+
def has_any_scope?(*scopes)
|
151
|
+
return false unless current_auth_context
|
152
|
+
|
153
|
+
scopes.any? { |scope| has_scope?(scope) }
|
154
|
+
end
|
155
|
+
|
156
|
+
# Require specific scopes for the current request
|
157
|
+
def require_scopes!(*scopes)
|
158
|
+
missing_scopes = scopes.reject { |scope| has_scope?(scope) }
|
159
|
+
|
160
|
+
if missing_scopes.any?
|
161
|
+
halt 403, {
|
162
|
+
error: 'insufficient_scope',
|
163
|
+
error_description: "Missing required scopes: #{missing_scopes.join(', ')}"
|
164
|
+
}.to_json
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
# Extract token from Authorization header
|
169
|
+
def extract_bearer_token
|
170
|
+
auth_header = request.env['HTTP_AUTHORIZATION']
|
171
|
+
return nil unless auth_header
|
172
|
+
|
173
|
+
match = auth_header.match(/\ABearer\s+(.+)\z/i)
|
174
|
+
match ? match[1] : nil
|
175
|
+
end
|
176
|
+
|
177
|
+
# Validate token directly (useful for custom logic)
|
178
|
+
def validate_oauth2_token(token, scheme: :oauth2_auth0)
|
179
|
+
auth_scheme = settings.rapitapir_config.auth_schemes[scheme]
|
180
|
+
return nil unless auth_scheme
|
181
|
+
|
182
|
+
begin
|
183
|
+
auth_scheme.authenticate(
|
184
|
+
OpenStruct.new(env: { 'HTTP_AUTHORIZATION' => "Bearer #{token}" })
|
185
|
+
)
|
186
|
+
rescue StandardError
|
187
|
+
nil
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
# DSL extensions for endpoint definitions with OAuth2
|
193
|
+
module OAuth2EndpointExtensions
|
194
|
+
# Add OAuth2 authentication to an endpoint
|
195
|
+
def with_oauth2_auth(scopes: [], scheme: :oauth2_auth0, description: nil)
|
196
|
+
auth_scheme_obj = case scheme
|
197
|
+
when Symbol
|
198
|
+
# Will be resolved at runtime
|
199
|
+
OpenStruct.new(name: scheme, scopes: scopes)
|
200
|
+
else
|
201
|
+
scheme
|
202
|
+
end
|
203
|
+
|
204
|
+
security_in(auth_scheme_obj).tap do |endpoint|
|
205
|
+
# Add to endpoint metadata for OpenAPI generation
|
206
|
+
endpoint.metadata[:security] ||= []
|
207
|
+
endpoint.metadata[:security] << {
|
208
|
+
scheme: scheme,
|
209
|
+
scopes: scopes,
|
210
|
+
description: description || "OAuth2 authentication with scopes: #{scopes.join(', ')}"
|
211
|
+
}
|
212
|
+
end
|
213
|
+
end
|
214
|
+
|
215
|
+
# Add Auth0 OAuth2 authentication to an endpoint
|
216
|
+
def with_auth0(scopes: [], description: nil)
|
217
|
+
with_oauth2_auth(scopes: scopes, scheme: :oauth2_auth0, description: description)
|
218
|
+
end
|
219
|
+
|
220
|
+
# Add scope requirement to an endpoint
|
221
|
+
def require_scopes(*scopes)
|
222
|
+
with_oauth2_auth(scopes: scopes.flatten)
|
223
|
+
end
|
224
|
+
end
|
225
|
+
end
|
226
|
+
end
|
227
|
+
|
228
|
+
# Extend the Sinatra extension with OAuth2 helpers if it exists
|
229
|
+
if defined?(RapiTapir::Sinatra::Extension)
|
230
|
+
RapiTapir::Sinatra::Extension::ClassMethods.include(RapiTapir::Sinatra::OAuth2Helpers)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Extend endpoint building with OAuth2 methods
|
234
|
+
if defined?(RapiTapir::DSL::FluentEndpointBuilder)
|
235
|
+
RapiTapir::DSL::FluentEndpointBuilder.include(RapiTapir::Sinatra::OAuth2EndpointExtensions)
|
236
|
+
end
|