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,158 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Core
|
5
|
+
# Core endpoint definition representing an HTTP API endpoint
|
6
|
+
#
|
7
|
+
# Encapsulates the definition of an HTTP endpoint including its method, path,
|
8
|
+
# input parameters, output formats, error responses, and metadata.
|
9
|
+
#
|
10
|
+
# @example Create an endpoint
|
11
|
+
# endpoint = RapiTapir::Core::Endpoint.new(
|
12
|
+
# method: :get,
|
13
|
+
# path: '/users/{id}',
|
14
|
+
# inputs: [path_param(:id, :integer)],
|
15
|
+
# outputs: [json_output(user_schema)]
|
16
|
+
# )
|
17
|
+
class Endpoint
|
18
|
+
HTTP_METHODS = %i[get post put patch delete options head].freeze
|
19
|
+
|
20
|
+
attr_reader :method, :path, :inputs, :outputs, :errors, :metadata
|
21
|
+
|
22
|
+
def initialize(**options)
|
23
|
+
@method = options[:method]
|
24
|
+
@path = options[:path]
|
25
|
+
@inputs = (options[:inputs] || []).freeze
|
26
|
+
@outputs = (options[:outputs] || []).freeze
|
27
|
+
@errors = (options[:errors] || []).freeze
|
28
|
+
@metadata = (options[:metadata] || {}).freeze
|
29
|
+
end
|
30
|
+
|
31
|
+
HTTP_METHODS.each do |http_method|
|
32
|
+
define_singleton_method(http_method) do |path = nil|
|
33
|
+
new(method: http_method, path: path)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
def in(input)
|
38
|
+
validate_input!(input)
|
39
|
+
copy_with(inputs: inputs + [input])
|
40
|
+
end
|
41
|
+
|
42
|
+
def out(output)
|
43
|
+
validate_output!(output)
|
44
|
+
copy_with(outputs: outputs + [output])
|
45
|
+
end
|
46
|
+
|
47
|
+
def error_out(code, output, **options)
|
48
|
+
validate_status_code!(code)
|
49
|
+
validate_output!(output)
|
50
|
+
error_entry = { code: code, output: output }.merge(options)
|
51
|
+
copy_with(errors: errors + [error_entry])
|
52
|
+
end
|
53
|
+
|
54
|
+
def with_metadata(**meta)
|
55
|
+
copy_with(metadata: metadata.merge(meta))
|
56
|
+
end
|
57
|
+
|
58
|
+
def description(text)
|
59
|
+
with_metadata(description: text)
|
60
|
+
end
|
61
|
+
|
62
|
+
def summary(text)
|
63
|
+
with_metadata(summary: text)
|
64
|
+
end
|
65
|
+
|
66
|
+
def tag(name)
|
67
|
+
with_metadata(tag: name)
|
68
|
+
end
|
69
|
+
|
70
|
+
def deprecated(*args, **kwargs)
|
71
|
+
# Support both deprecated(true/false) and deprecated(flag: true/false)
|
72
|
+
flag_value = if args.length.positive?
|
73
|
+
args.first
|
74
|
+
else
|
75
|
+
kwargs.fetch(:flag, true)
|
76
|
+
end
|
77
|
+
with_metadata(deprecated: flag_value)
|
78
|
+
end
|
79
|
+
|
80
|
+
# Validate input/output types for a given input/output hash
|
81
|
+
# Raises validation errors if invalid, returns true if valid
|
82
|
+
def validate!(input_hash = {}, output_hash = {}) # rubocop:disable Naming/PredicateMethod
|
83
|
+
validate_inputs!(input_hash)
|
84
|
+
validate_outputs!(output_hash) unless output_hash.empty?
|
85
|
+
true
|
86
|
+
end
|
87
|
+
|
88
|
+
def to_h
|
89
|
+
{
|
90
|
+
method: method,
|
91
|
+
path: path,
|
92
|
+
inputs: inputs.map(&:to_h),
|
93
|
+
outputs: outputs.map(&:to_h),
|
94
|
+
errors: errors,
|
95
|
+
metadata: metadata
|
96
|
+
}
|
97
|
+
end
|
98
|
+
|
99
|
+
private
|
100
|
+
|
101
|
+
def copy_with(**changes)
|
102
|
+
self.class.new(
|
103
|
+
method: changes.fetch(:method, method),
|
104
|
+
path: changes.fetch(:path, path),
|
105
|
+
inputs: changes.fetch(:inputs, inputs),
|
106
|
+
outputs: changes.fetch(:outputs, outputs),
|
107
|
+
errors: changes.fetch(:errors, errors),
|
108
|
+
metadata: changes.fetch(:metadata, metadata)
|
109
|
+
)
|
110
|
+
end
|
111
|
+
|
112
|
+
def validate_input!(input)
|
113
|
+
return if input.respond_to?(:kind) && input.respond_to?(:name) && input.respond_to?(:type)
|
114
|
+
|
115
|
+
raise ArgumentError, 'Input must respond to :kind, :name, and :type'
|
116
|
+
end
|
117
|
+
|
118
|
+
def validate_output!(output)
|
119
|
+
return if output.respond_to?(:kind) && output.respond_to?(:type)
|
120
|
+
|
121
|
+
raise ArgumentError, 'Output must respond to :kind and :type'
|
122
|
+
end
|
123
|
+
|
124
|
+
def validate_status_code!(code)
|
125
|
+
return if code.is_a?(Integer) && code >= 100 && code <= 599
|
126
|
+
|
127
|
+
raise ArgumentError, "Invalid status code: #{code}. Must be an integer between 100-599"
|
128
|
+
end
|
129
|
+
|
130
|
+
def validate_inputs!(input_hash)
|
131
|
+
inputs.each do |input|
|
132
|
+
next unless input_hash.key?(input.name)
|
133
|
+
|
134
|
+
unless input.valid_type?(input_hash[input.name])
|
135
|
+
raise TypeError,
|
136
|
+
"Invalid type for input '#{input.name}': expected #{input.type}, got #{input_hash[input.name].class}"
|
137
|
+
end
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
def validate_outputs!(output_hash)
|
142
|
+
outputs.each do |output|
|
143
|
+
if output.type.is_a?(Hash)
|
144
|
+
unless output.valid_type?(output_hash)
|
145
|
+
raise TypeError, "Invalid output hash: expected #{output.type}, got #{output_hash}"
|
146
|
+
end
|
147
|
+
else
|
148
|
+
output_hash.each do |k, v|
|
149
|
+
unless output.valid_type?(v)
|
150
|
+
raise TypeError, "Invalid type for output '#{k}': expected #{output.type}, got #{v.class}"
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'endpoint'
|
4
|
+
require_relative '../dsl/enhanced_endpoint_dsl'
|
5
|
+
require_relative '../types'
|
6
|
+
require_relative '../schema'
|
7
|
+
|
8
|
+
module RapiTapir
|
9
|
+
module Core
|
10
|
+
# Enhanced Endpoint class that integrates with the new type system
|
11
|
+
class EnhancedEndpoint < Endpoint
|
12
|
+
attr_reader :security_schemes, :custom_validators
|
13
|
+
|
14
|
+
def initialize(**options)
|
15
|
+
super
|
16
|
+
@security_schemes = []
|
17
|
+
@custom_validators = []
|
18
|
+
end
|
19
|
+
|
20
|
+
# Type validation helpers
|
21
|
+
def validate_with_type(type, value)
|
22
|
+
return true if type.nil?
|
23
|
+
|
24
|
+
type.validate(value)
|
25
|
+
true
|
26
|
+
rescue Types::CoercionError => e
|
27
|
+
raise ValidationError, "Type validation failed: #{e.message}"
|
28
|
+
end
|
29
|
+
|
30
|
+
# Enhanced error handling with typed errors
|
31
|
+
def error_out(status_code, error_type_def, **options)
|
32
|
+
error_type = resolve_type(error_type_def)
|
33
|
+
error_output = DSL::EnhancedOutput.new(
|
34
|
+
kind: :json,
|
35
|
+
type: error_type,
|
36
|
+
options: options.merge(status_code: status_code)
|
37
|
+
)
|
38
|
+
|
39
|
+
error_entry = {
|
40
|
+
code: status_code,
|
41
|
+
output: error_output,
|
42
|
+
type: error_type
|
43
|
+
}.merge(options)
|
44
|
+
|
45
|
+
copy_with(errors: errors + [error_entry])
|
46
|
+
end
|
47
|
+
|
48
|
+
# Security integration
|
49
|
+
def security_in(auth_scheme)
|
50
|
+
copy_with(
|
51
|
+
inputs: inputs + [auth_scheme],
|
52
|
+
security_schemes: security_schemes + [auth_scheme]
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Validation integration
|
57
|
+
def validate_request_with(validator)
|
58
|
+
new_validators = custom_validators + [validator]
|
59
|
+
copy_with_validators(new_validators)
|
60
|
+
end
|
61
|
+
|
62
|
+
# Process a request with full type validation
|
63
|
+
def process_request(extracted_inputs)
|
64
|
+
run_custom_validators(extracted_inputs)
|
65
|
+
validate_input_values(extracted_inputs)
|
66
|
+
extracted_inputs
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
|
71
|
+
def run_custom_validators(extracted_inputs)
|
72
|
+
custom_validators.each do |validator|
|
73
|
+
validator.call(extracted_inputs)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def validate_input_values(extracted_inputs)
|
78
|
+
validation_errors = collect_input_validation_errors(extracted_inputs)
|
79
|
+
raise ValidationError, build_validation_error_message(validation_errors) if validation_errors.any?
|
80
|
+
end
|
81
|
+
|
82
|
+
def collect_input_validation_errors(extracted_inputs)
|
83
|
+
validation_errors = []
|
84
|
+
|
85
|
+
inputs.each do |input|
|
86
|
+
next unless extracted_inputs.key?(input.name)
|
87
|
+
|
88
|
+
value = extracted_inputs[input.name]
|
89
|
+
result = input.validate(value)
|
90
|
+
next if result[:valid]
|
91
|
+
|
92
|
+
add_input_errors_to_collection(validation_errors, input, result[:errors])
|
93
|
+
end
|
94
|
+
|
95
|
+
validation_errors
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_input_errors_to_collection(validation_errors, input, errors)
|
99
|
+
errors.each do |error|
|
100
|
+
validation_errors << "Input '#{input.name}': #{error}"
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
def build_validation_error_message(validation_errors)
|
105
|
+
"Request validation failed:\n#{validation_errors.join("\n")}"
|
106
|
+
end
|
107
|
+
|
108
|
+
public
|
109
|
+
|
110
|
+
# Generate OpenAPI specification for this endpoint
|
111
|
+
def to_openapi_spec
|
112
|
+
spec = build_base_spec
|
113
|
+
add_parameters_to_spec(spec)
|
114
|
+
add_request_body_to_spec(spec)
|
115
|
+
add_responses_to_spec(spec)
|
116
|
+
add_error_responses_to_spec(spec)
|
117
|
+
add_security_to_spec(spec)
|
118
|
+
spec.compact
|
119
|
+
end
|
120
|
+
|
121
|
+
# Enhanced validation with detailed errors
|
122
|
+
def validate!
|
123
|
+
super
|
124
|
+
|
125
|
+
# Additional validations for enhanced features
|
126
|
+
validate_security_schemes!
|
127
|
+
validate_type_consistency!
|
128
|
+
end
|
129
|
+
|
130
|
+
private
|
131
|
+
|
132
|
+
def build_base_spec
|
133
|
+
{
|
134
|
+
summary: metadata[:summary],
|
135
|
+
description: metadata[:description],
|
136
|
+
tags: Array(metadata[:tags]),
|
137
|
+
parameters: [],
|
138
|
+
responses: {}
|
139
|
+
}
|
140
|
+
end
|
141
|
+
|
142
|
+
def add_parameters_to_spec(spec)
|
143
|
+
inputs.each do |input|
|
144
|
+
next if input.kind == :body
|
145
|
+
|
146
|
+
spec[:parameters] << input.to_openapi_parameter
|
147
|
+
end
|
148
|
+
end
|
149
|
+
|
150
|
+
def add_request_body_to_spec(spec)
|
151
|
+
body_input = inputs.find { |i| i.kind == :body }
|
152
|
+
return unless body_input
|
153
|
+
|
154
|
+
spec[:requestBody] = {
|
155
|
+
required: body_input.required?,
|
156
|
+
content: {
|
157
|
+
'application/json' => {
|
158
|
+
schema: body_input.type.to_json_schema
|
159
|
+
}
|
160
|
+
}
|
161
|
+
}
|
162
|
+
end
|
163
|
+
|
164
|
+
def add_responses_to_spec(spec)
|
165
|
+
outputs.each do |output|
|
166
|
+
next if output.kind == :status
|
167
|
+
|
168
|
+
status_code = find_status_code || 200
|
169
|
+
spec[:responses][status_code.to_s] = output.to_openapi_response
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
def add_error_responses_to_spec(spec)
|
174
|
+
errors.each do |error|
|
175
|
+
spec[:responses][error[:code].to_s] = error[:output].to_openapi_response
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def add_security_to_spec(spec)
|
180
|
+
return unless security_schemes.any?
|
181
|
+
|
182
|
+
spec[:security] = security_schemes.map do |scheme|
|
183
|
+
map_security_scheme_to_openapi(scheme)
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
def map_security_scheme_to_openapi(scheme)
|
188
|
+
case scheme.options[:auth_type]
|
189
|
+
when :bearer
|
190
|
+
{ 'bearerAuth' => [] }
|
191
|
+
when :api_key
|
192
|
+
{ 'apiKeyAuth' => [] }
|
193
|
+
when :basic
|
194
|
+
{ 'basicAuth' => [] }
|
195
|
+
else
|
196
|
+
{}
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
def copy_with_validators(new_validators)
|
201
|
+
new_endpoint = copy_with({})
|
202
|
+
new_endpoint.instance_variable_set(:@custom_validators, new_validators)
|
203
|
+
new_endpoint
|
204
|
+
end
|
205
|
+
|
206
|
+
def find_status_code
|
207
|
+
status_output = outputs.find { |o| o.kind == :status }
|
208
|
+
status_output&.type
|
209
|
+
end
|
210
|
+
|
211
|
+
def validate_security_schemes!
|
212
|
+
security_schemes.each do |scheme|
|
213
|
+
unless %i[bearer api_key basic].include?(scheme.options[:auth_type])
|
214
|
+
raise ArgumentError, "Unknown authentication type: #{scheme.options[:auth_type]}"
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
def validate_type_consistency!
|
220
|
+
inputs.each do |input|
|
221
|
+
unless input.type.respond_to?(:validate)
|
222
|
+
raise ArgumentError, "Input '#{input.name}' type does not support validation"
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
outputs.each do |output|
|
227
|
+
next if output.kind == :status
|
228
|
+
raise ArgumentError, 'Output type does not support validation' unless output.type.respond_to?(:validate)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
class ValidationError < StandardError; end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,182 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RapiTapir
|
4
|
+
module Core
|
5
|
+
# Input parameter definition for HTTP endpoints
|
6
|
+
#
|
7
|
+
# Represents different types of input parameters that an endpoint can accept,
|
8
|
+
# including query parameters, path parameters, headers, and request body.
|
9
|
+
#
|
10
|
+
# @example Query parameter
|
11
|
+
# RapiTapir::Core::Input.new(kind: :query, name: :limit, type: :integer)
|
12
|
+
#
|
13
|
+
# @example Path parameter
|
14
|
+
# RapiTapir::Core::Input.new(kind: :path, name: :id, type: :string)
|
15
|
+
class Input
|
16
|
+
VALID_KINDS = %i[query path header body].freeze
|
17
|
+
VALID_TYPES = %i[string integer float boolean date datetime].freeze
|
18
|
+
|
19
|
+
attr_reader :kind, :name, :type, :options
|
20
|
+
|
21
|
+
def initialize(kind:, name:, type:, options: {})
|
22
|
+
validate_kind!(kind)
|
23
|
+
validate_name!(name)
|
24
|
+
validate_type!(type)
|
25
|
+
|
26
|
+
@kind = kind
|
27
|
+
@name = name.to_sym
|
28
|
+
@type = type
|
29
|
+
@options = options.freeze
|
30
|
+
end
|
31
|
+
|
32
|
+
def required?
|
33
|
+
!options[:optional]
|
34
|
+
end
|
35
|
+
|
36
|
+
def optional?
|
37
|
+
!!options[:optional]
|
38
|
+
end
|
39
|
+
|
40
|
+
def valid_type?(value)
|
41
|
+
return true if value.nil? && optional?
|
42
|
+
|
43
|
+
validate_type_match(value, type)
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_type_match(value, target_type)
|
47
|
+
case target_type
|
48
|
+
when :string then value.is_a?(String)
|
49
|
+
when :integer then value.is_a?(Integer)
|
50
|
+
when :float then numeric_value?(value)
|
51
|
+
when :boolean then boolean_value?(value)
|
52
|
+
else
|
53
|
+
validate_complex_type_match(value, target_type)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def validate_complex_type_match(value, target_type)
|
58
|
+
case target_type
|
59
|
+
when :date then validate_date_type(value)
|
60
|
+
when :datetime then validate_datetime_type(value)
|
61
|
+
when Hash then validate_hash_type(value)
|
62
|
+
when Class then value.is_a?(target_type)
|
63
|
+
else true # Accept any for custom types
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
def numeric_value?(value)
|
68
|
+
value.is_a?(Float) || value.is_a?(Integer)
|
69
|
+
end
|
70
|
+
|
71
|
+
def boolean_value?(value)
|
72
|
+
[true, false].include?(value)
|
73
|
+
end
|
74
|
+
|
75
|
+
def validate_date_type(value)
|
76
|
+
value.is_a?(Date) || (value.is_a?(String) && date_string?(value))
|
77
|
+
end
|
78
|
+
|
79
|
+
def validate_datetime_type(value)
|
80
|
+
value.is_a?(DateTime) || (value.is_a?(String) && datetime_string?(value))
|
81
|
+
end
|
82
|
+
|
83
|
+
def coerce(value)
|
84
|
+
return nil if value.nil? && optional?
|
85
|
+
|
86
|
+
coerce_by_type(value, type)
|
87
|
+
rescue ArgumentError => e
|
88
|
+
raise TypeError, "Cannot coerce #{value.inspect} to #{type}: #{e.message}"
|
89
|
+
end
|
90
|
+
|
91
|
+
def coerce_by_type(value, target_type)
|
92
|
+
case target_type
|
93
|
+
when :string then value.to_s
|
94
|
+
when :integer then Integer(value)
|
95
|
+
when :float then Float(value)
|
96
|
+
when :boolean then !!value
|
97
|
+
when :date then coerce_to_date(value)
|
98
|
+
when :datetime then coerce_to_datetime(value)
|
99
|
+
else value
|
100
|
+
end
|
101
|
+
end
|
102
|
+
|
103
|
+
def coerce_to_date(value)
|
104
|
+
value.is_a?(Date) ? value : Date.parse(value.to_s)
|
105
|
+
end
|
106
|
+
|
107
|
+
def coerce_to_datetime(value)
|
108
|
+
value.is_a?(DateTime) ? value : DateTime.parse(value.to_s)
|
109
|
+
end
|
110
|
+
|
111
|
+
def to_h
|
112
|
+
{
|
113
|
+
kind: kind,
|
114
|
+
name: name,
|
115
|
+
type: type,
|
116
|
+
options: options,
|
117
|
+
required: required?
|
118
|
+
}
|
119
|
+
end
|
120
|
+
|
121
|
+
private
|
122
|
+
|
123
|
+
def validate_kind!(kind)
|
124
|
+
return if VALID_KINDS.include?(kind)
|
125
|
+
|
126
|
+
raise ArgumentError, "Invalid kind: #{kind}. Must be one of #{VALID_KINDS}"
|
127
|
+
end
|
128
|
+
|
129
|
+
def validate_name!(name)
|
130
|
+
return unless name.nil? || (name.respond_to?(:empty?) && name.empty?)
|
131
|
+
|
132
|
+
raise ArgumentError, 'Input name cannot be nil or empty'
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_type!(type)
|
136
|
+
return if VALID_TYPES.include?(type) || type.is_a?(Hash) || type.is_a?(Class)
|
137
|
+
|
138
|
+
raise ArgumentError, "Invalid type: #{type}. Must be one of #{VALID_TYPES}, a Hash, or a Class"
|
139
|
+
end
|
140
|
+
|
141
|
+
def validate_hash_type(value)
|
142
|
+
return false unless value.is_a?(Hash)
|
143
|
+
|
144
|
+
type.all? do |key, expected_type|
|
145
|
+
valid_hash_field_type?(value[key], expected_type)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
def valid_hash_field_type?(field_value, expected_type)
|
150
|
+
case expected_type
|
151
|
+
when :string then field_value.is_a?(String)
|
152
|
+
when :integer then field_value.is_a?(Integer)
|
153
|
+
when :float then numeric_field_value?(field_value)
|
154
|
+
when :boolean then boolean_field_value?(field_value)
|
155
|
+
else true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
def numeric_field_value?(field_value)
|
160
|
+
field_value.is_a?(Float) || field_value.is_a?(Integer)
|
161
|
+
end
|
162
|
+
|
163
|
+
def boolean_field_value?(field_value)
|
164
|
+
[true, false].include?(field_value)
|
165
|
+
end
|
166
|
+
|
167
|
+
def date_string?(value)
|
168
|
+
Date.parse(value.to_s)
|
169
|
+
true
|
170
|
+
rescue ArgumentError
|
171
|
+
false
|
172
|
+
end
|
173
|
+
|
174
|
+
def datetime_string?(value)
|
175
|
+
DateTime.parse(value.to_s)
|
176
|
+
true
|
177
|
+
rescue ArgumentError
|
178
|
+
false
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|