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