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,265 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'ostruct'
4
+
5
+ module RapiTapir
6
+ module Types
7
+ # Auto-derivation module for creating RapiTapir type schemas from various sources
8
+ #
9
+ # This module provides automatic type inference and schema generation from:
10
+ # - Ruby hashes with sample values
11
+ # - JSON Schema documents
12
+ # - OpenStruct instances
13
+ # - Protocol Buffer message classes
14
+ #
15
+ # All methods support field filtering via only/except parameters.
16
+ # rubocop:disable Metrics/ModuleLength
17
+ module AutoDerivation
18
+ class << self
19
+ # Derive schema from a plain Hash with sample values
20
+ #
21
+ # @param hash [Hash] Hash with sample values for type inference
22
+ # @param only [Array<Symbol>] Only include these fields
23
+ # @param except [Array<Symbol>] Exclude these fields
24
+ # @return [RapiTapir::Types::Hash]
25
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
26
+ def from_hash(hash, only: nil, except: nil)
27
+ raise ArgumentError, "Expected Hash, got #{hash.class}" unless hash.is_a?(::Hash)
28
+
29
+ schema_hash = {}
30
+ hash.each do |field_name, value|
31
+ # Apply field filtering
32
+ field_sym = field_name.to_sym
33
+ next if only && !Array(only).map(&:to_sym).include?(field_sym)
34
+ next if except && Array(except).map(&:to_sym).include?(field_sym)
35
+
36
+ rapitapir_type = infer_type_from_value(value)
37
+ schema_hash[field_name.to_s] = rapitapir_type
38
+ end
39
+
40
+ create_hash_type(schema_hash)
41
+ end
42
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
43
+
44
+ # Derive schema from JSON Schema object
45
+ #
46
+ # @param json_schema [Hash] JSON Schema object
47
+ # @param only [Array<Symbol>] Only include these fields
48
+ # @param except [Array<Symbol>] Exclude these fields
49
+ # @return [RapiTapir::Types::Hash]
50
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
51
+ def from_json_schema(json_schema, only: nil, except: nil)
52
+ unless json_schema.is_a?(::Hash) && json_schema['type'] == 'object'
53
+ raise ArgumentError, 'JSON Schema must be an object type'
54
+ end
55
+
56
+ properties = json_schema['properties'] || {}
57
+ required_fields = Array(json_schema['required'])
58
+ schema_hash = {}
59
+
60
+ properties.each do |field_name, field_schema|
61
+ field_sym = field_name.to_sym
62
+ next if only && !Array(only).map(&:to_sym).include?(field_sym)
63
+ next if except && Array(except).map(&:to_sym).include?(field_sym)
64
+
65
+ required = required_fields.include?(field_name)
66
+ rapitapir_type = convert_json_schema_type(field_schema, required: required)
67
+ schema_hash[field_name] = rapitapir_type
68
+ end
69
+
70
+ RapiTapir::Types.hash(schema_hash)
71
+ end
72
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
73
+
74
+ # Derive schema from OpenStruct instance
75
+ #
76
+ # @param open_struct [OpenStruct] OpenStruct with populated fields
77
+ # @param only [Array<Symbol>] Only include these fields
78
+ # @param except [Array<Symbol>] Exclude these fields
79
+ # @return [RapiTapir::Types::Hash]
80
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Style/OpenStructUse
81
+ def from_open_struct(open_struct, only: nil, except: nil)
82
+ raise ArgumentError, "Expected OpenStruct, got #{open_struct.class}" unless open_struct.is_a?(OpenStruct)
83
+
84
+ schema_hash = {}
85
+ open_struct.to_h.each do |field_name, value|
86
+ field_sym = field_name.to_sym
87
+ next if only && !Array(only).map(&:to_sym).include?(field_sym)
88
+ next if except && Array(except).map(&:to_sym).include?(field_sym)
89
+
90
+ rapitapir_type = infer_type_from_value(value)
91
+ schema_hash[field_name.to_s] = rapitapir_type
92
+ end
93
+
94
+ create_hash_type(schema_hash)
95
+ end
96
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Style/OpenStructUse
97
+
98
+ # Derive schema from Protobuf message class
99
+ #
100
+ # @param proto_class [Class] Protobuf message class
101
+ # @param only [Array<Symbol>] Only include these fields
102
+ # @param except [Array<Symbol>] Exclude these fields
103
+ # @return [RapiTapir::Types::Hash]
104
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105
+ def from_protobuf(proto_class, only: nil, except: nil)
106
+ unless defined?(Google::Protobuf) && proto_class.respond_to?(:descriptor)
107
+ raise ArgumentError, 'Protobuf not available or invalid protobuf class'
108
+ end
109
+
110
+ schema_hash = {}
111
+ proto_class.descriptor.each do |field_descriptor|
112
+ field_name = field_descriptor.name
113
+ field_sym = field_name.to_sym
114
+
115
+ next if only && !Array(only).map(&:to_sym).include?(field_sym)
116
+ next if except && Array(except).map(&:to_sym).include?(field_sym)
117
+
118
+ rapitapir_type = convert_protobuf_type(field_descriptor)
119
+ schema_hash[field_name] = rapitapir_type
120
+ end
121
+
122
+ create_hash_type(schema_hash)
123
+ end
124
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
125
+
126
+ private
127
+
128
+ # Convert JSON Schema field type to RapiTapir type
129
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
130
+ def convert_json_schema_type(field_schema, required: true)
131
+ base_type = case field_schema['type']
132
+ when 'string'
133
+ case field_schema['format']
134
+ when 'email'
135
+ create_email_type
136
+ when 'uuid'
137
+ create_uuid_type
138
+ when 'date'
139
+ create_date_type
140
+ when 'date-time'
141
+ create_datetime_type
142
+ else
143
+ create_string_type
144
+ end
145
+ when 'integer'
146
+ create_integer_type
147
+ when 'number'
148
+ create_float_type
149
+ when 'boolean'
150
+ create_boolean_type
151
+ when 'array'
152
+ item_type = if field_schema['items']
153
+ convert_json_schema_type(field_schema['items'], required: true)
154
+ else
155
+ create_string_type
156
+ end
157
+ create_array_type(item_type)
158
+ when 'object'
159
+ create_hash_type({})
160
+ else
161
+ create_string_type
162
+ end
163
+
164
+ required ? base_type : create_optional_type(base_type)
165
+ end
166
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
167
+
168
+ # Convert protobuf field descriptor to RapiTapir type
169
+ def convert_protobuf_type(field_descriptor)
170
+ base_type = case field_descriptor.type
171
+ when :TYPE_INT32, :TYPE_INT64, :TYPE_UINT32, :TYPE_UINT64
172
+ create_integer_type
173
+ when :TYPE_FLOAT, :TYPE_DOUBLE
174
+ create_float_type
175
+ when :TYPE_BOOL
176
+ create_boolean_type
177
+ when :TYPE_MESSAGE
178
+ create_hash_type({})
179
+ else
180
+ # :TYPE_STRING, :TYPE_BYTES, :TYPE_ENUM, and others default to string
181
+ create_string_type
182
+ end
183
+
184
+ if field_descriptor.label == :LABEL_REPEATED
185
+ create_array_type(base_type)
186
+ else
187
+ base_type
188
+ end
189
+ end
190
+
191
+ # Infer RapiTapir type from Ruby value
192
+ # rubocop:disable Metrics/CyclomaticComplexity
193
+ def infer_type_from_value(value)
194
+ case value
195
+ when ::Integer
196
+ create_integer_type
197
+ when ::Float
198
+ create_float_type
199
+ when TrueClass, FalseClass
200
+ create_boolean_type
201
+ when ::Date
202
+ create_date_type
203
+ when ::Time, ::DateTime
204
+ create_datetime_type
205
+ when ::Array
206
+ item_type = value.empty? ? create_string_type : infer_type_from_value(value.first)
207
+ create_array_type(item_type)
208
+ when ::Hash
209
+ create_hash_type({})
210
+ else
211
+ # ::String, nil, and other unknown types default to string
212
+ create_string_type
213
+ end
214
+ end
215
+ # rubocop:enable Metrics/CyclomaticComplexity
216
+
217
+ # Helper methods to create types without using convenience methods
218
+ def create_string_type
219
+ RapiTapir::Types::String.new
220
+ end
221
+
222
+ def create_integer_type
223
+ RapiTapir::Types::Integer.new
224
+ end
225
+
226
+ def create_float_type
227
+ RapiTapir::Types::Float.new
228
+ end
229
+
230
+ def create_boolean_type
231
+ RapiTapir::Types::Boolean.new
232
+ end
233
+
234
+ def create_date_type
235
+ RapiTapir::Types::Date.new
236
+ end
237
+
238
+ def create_datetime_type
239
+ RapiTapir::Types::DateTime.new
240
+ end
241
+
242
+ def create_email_type
243
+ RapiTapir::Types::Email.new
244
+ end
245
+
246
+ def create_uuid_type
247
+ RapiTapir::Types::UUID.new
248
+ end
249
+
250
+ def create_array_type(item_type)
251
+ RapiTapir::Types::Array.new(item_type)
252
+ end
253
+
254
+ def create_hash_type(field_types)
255
+ RapiTapir::Types::Hash.new(field_types)
256
+ end
257
+
258
+ def create_optional_type(base_type)
259
+ RapiTapir::Types::Optional.new(base_type)
260
+ end
261
+ end
262
+ end
263
+ # rubocop:enable Metrics/ModuleLength
264
+ end
265
+ end
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Types
5
+ # Base class for all RapiTapir types
6
+ #
7
+ # Provides the fundamental interface for type validation, coercion,
8
+ # constraint checking, and schema generation that all specific types inherit.
9
+ #
10
+ # @abstract Subclass and implement {#validate_constraints} and {#coerce_value}
11
+ # @example Creating a custom type
12
+ # class CustomType < Base
13
+ # def validate_constraints(value)
14
+ # # Validation logic
15
+ # end
16
+ #
17
+ # def coerce_value(value)
18
+ # # Coercion logic
19
+ # end
20
+ # end
21
+ class Base
22
+ attr_reader :constraints, :metadata
23
+
24
+ def initialize(**constraints)
25
+ @constraints = constraints.freeze
26
+ @metadata = {}
27
+ end
28
+
29
+ # Validate a value against this type
30
+ def validate(value)
31
+ errors = []
32
+
33
+ # Check if value is required but nil
34
+ if value.nil? && required?
35
+ errors << 'Value is required but got nil'
36
+ return validation_result(false, errors)
37
+ end
38
+
39
+ # Allow nil for optional types
40
+ return validation_result(true, []) if value.nil? && !required?
41
+
42
+ # Perform type-specific validation
43
+ type_errors = validate_type(value)
44
+ errors.concat(type_errors)
45
+
46
+ # Perform constraint validation
47
+ constraint_errors = validate_constraints(value)
48
+ errors.concat(constraint_errors)
49
+
50
+ validation_result(errors.empty?, errors)
51
+ end
52
+
53
+ # Coerce a value to this type
54
+ def coerce(value)
55
+ return nil if value.nil? && !required?
56
+
57
+ raise CoercionError.new(value, self.class.name, 'Required value cannot be nil') if value.nil? && required?
58
+
59
+ coerce_value(value)
60
+ end
61
+
62
+ # Check if this type is required
63
+ def required?
64
+ !constraints.fetch(:optional, false)
65
+ end
66
+
67
+ # Check if this type is optional
68
+ def optional?
69
+ constraints.fetch(:optional, false)
70
+ end
71
+
72
+ # Get the JSON Schema representation
73
+ def to_json_schema
74
+ schema = base_json_schema
75
+ apply_constraints_to_schema(schema)
76
+ schema
77
+ end
78
+
79
+ # Get a string representation
80
+ def to_s
81
+ constraint_strs = constraints.map { |k, v| "#{k}: #{v}" }
82
+ constraint_part = constraint_strs.empty? ? '' : "(#{constraint_strs.join(', ')})"
83
+ "#{self.class.name.split('::').last}#{constraint_part}"
84
+ end
85
+
86
+ # Add metadata to this type
87
+ def with_metadata(**meta)
88
+ dup.tap { |type| type.instance_variable_set(:@metadata, metadata.merge(meta)) }
89
+ end
90
+
91
+ # Add description to this type
92
+ def description(text)
93
+ with_metadata(description: text)
94
+ end
95
+
96
+ # Add example to this type
97
+ def example(value)
98
+ with_metadata(example: value)
99
+ end
100
+
101
+ protected
102
+
103
+ # Override in subclasses to implement type-specific validation
104
+ def validate_type(_value)
105
+ []
106
+ end
107
+
108
+ # Override in subclasses to implement constraint validation
109
+ def validate_constraints(_value)
110
+ []
111
+ end
112
+
113
+ # Override in subclasses to implement value coercion
114
+ def coerce_value(value)
115
+ value
116
+ end
117
+
118
+ # Override in subclasses to provide base JSON schema
119
+ def base_json_schema
120
+ { type: json_type }
121
+ end
122
+
123
+ # Override in subclasses to specify JSON type
124
+ def json_type
125
+ 'object'
126
+ end
127
+
128
+ # Apply constraints to JSON schema
129
+ def apply_constraints_to_schema(schema)
130
+ # Add common constraint mappings here
131
+ schema[:description] = metadata[:description] if metadata[:description]
132
+ schema[:example] = metadata[:example] if metadata[:example]
133
+ end
134
+
135
+ private
136
+
137
+ def validation_result(valid, errors)
138
+ {
139
+ valid: valid,
140
+ errors: errors,
141
+ value_errors: errors.empty? ? [] : [ValidationError.new(nil, self, errors)]
142
+ }
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module RapiTapir
6
+ module Types
7
+ # Boolean type for validating true/false values
8
+ # Accepts true, false, and can coerce from string representations
9
+ class Boolean < Base
10
+ protected
11
+
12
+ def validate_type(value)
13
+ return [] if [true, false].include?(value)
14
+
15
+ ["Expected boolean (true or false), got #{value.class}"]
16
+ end
17
+
18
+ def validate_constraints(_value)
19
+ # Boolean types don't have additional constraints beyond true/false
20
+ []
21
+ end
22
+
23
+ def coerce_value(value)
24
+ case value
25
+ when true, false then value
26
+ when 'true', 'TRUE', '1', 1 then true
27
+ when 'false', 'FALSE', '0', 0 then false
28
+ when ::String
29
+ case value.strip.downcase
30
+ when 'true', 'yes', 'on', '1' then true
31
+ when 'false', 'no', 'off', '0' then false
32
+ else
33
+ raise CoercionError.new(value, 'Boolean', "Cannot convert '#{value}' to boolean")
34
+ end
35
+ else
36
+ # Use Ruby's truthiness as fallback
37
+ !!value
38
+ end
39
+ end
40
+
41
+ def json_type
42
+ 'boolean'
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'date'
5
+
6
+ module RapiTapir
7
+ module Types
8
+ # Date type for validating date values in various formats
9
+ # Accepts Date objects and string representations that can be parsed
10
+ class Date < Base
11
+ def initialize(format: nil, **options)
12
+ super
13
+ end
14
+
15
+ protected
16
+
17
+ def validate_type(value)
18
+ return [] if value.is_a?(::Date)
19
+ return [] if value.is_a?(::String) && parseable_date?(value)
20
+
21
+ ["Expected Date or date string, got #{value.class}"]
22
+ end
23
+
24
+ def validate_constraints(value)
25
+ errors = []
26
+
27
+ if constraints[:format] && value.is_a?(::String)
28
+ format_errors = validate_date_format(value, constraints[:format])
29
+ errors.concat(format_errors)
30
+ end
31
+
32
+ errors
33
+ end
34
+
35
+ def coerce_value(value)
36
+ case value
37
+ when ::Date then value
38
+ when ::DateTime, ::Time then value.to_date
39
+ when ::String
40
+ ::Date.parse(value)
41
+ when ::Integer
42
+ # Assume Unix timestamp
43
+ ::Time.at(value).to_date
44
+ else
45
+ raise CoercionError.new(value, 'Date', 'Value cannot be converted to Date') unless value.respond_to?(:to_date)
46
+
47
+ value.to_date
48
+
49
+ end
50
+ rescue ArgumentError => e
51
+ raise CoercionError.new(value, 'Date', e.message)
52
+ end
53
+
54
+ def json_type
55
+ 'string'
56
+ end
57
+
58
+ def apply_constraints_to_schema(schema)
59
+ super
60
+ schema[:format] = 'date'
61
+ schema[:format] = constraints[:format].to_s if constraints[:format]
62
+ end
63
+
64
+ private
65
+
66
+ def parseable_date?(value)
67
+ ::Date.parse(value)
68
+ true
69
+ rescue ArgumentError
70
+ false
71
+ end
72
+
73
+ def validate_date_format(value, format)
74
+ case format
75
+ when :iso8601, 'iso8601'
76
+ iso_date_pattern = /\A\d{4}-\d{2}-\d{2}\z/
77
+ iso_date_pattern.match?(value) ? [] : ['Date must be in ISO8601 format (YYYY-MM-DD)']
78
+ when String
79
+ # Custom format validation
80
+ begin
81
+ ::Date.strptime(value, format)
82
+ []
83
+ rescue ArgumentError
84
+ ["Date does not match format #{format}"]
85
+ end
86
+ else
87
+ []
88
+ end
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+ require 'date'
5
+
6
+ module RapiTapir
7
+ module Types
8
+ # DateTime type for validating datetime values with timezone handling
9
+ # Accepts DateTime objects and string representations in various formats
10
+ class DateTime < Base
11
+ def initialize(format: nil, **options)
12
+ super
13
+ end
14
+
15
+ protected
16
+
17
+ def validate_type(value)
18
+ return [] if value.is_a?(::DateTime) || value.is_a?(::Time)
19
+ return [] if value.is_a?(::String) && parseable_datetime?(value)
20
+
21
+ ["Expected DateTime, Time, or datetime string, got #{value.class}"]
22
+ end
23
+
24
+ def validate_constraints(value)
25
+ errors = []
26
+
27
+ if constraints[:format] && value.is_a?(::String)
28
+ format_errors = validate_datetime_format(value, constraints[:format])
29
+ errors.concat(format_errors)
30
+ end
31
+
32
+ errors
33
+ end
34
+
35
+ def coerce_value(value)
36
+ case value
37
+ when ::DateTime then value
38
+ when ::Time, ::Date then value.to_datetime
39
+ when ::String
40
+ ::DateTime.parse(value)
41
+ when ::Integer
42
+ # Assume Unix timestamp
43
+ ::Time.at(value).to_datetime
44
+ else
45
+ unless value.respond_to?(:to_datetime)
46
+ raise CoercionError.new(value, 'DateTime', 'Value cannot be converted to DateTime')
47
+ end
48
+
49
+ value.to_datetime
50
+
51
+ end
52
+ rescue ArgumentError => e
53
+ raise CoercionError.new(value, 'DateTime', e.message)
54
+ end
55
+
56
+ def json_type
57
+ 'string'
58
+ end
59
+
60
+ def apply_constraints_to_schema(schema)
61
+ super
62
+ schema[:format] = 'date-time'
63
+ schema[:format] = constraints[:format].to_s if constraints[:format]
64
+ end
65
+
66
+ private
67
+
68
+ def parseable_datetime?(value)
69
+ ::DateTime.parse(value)
70
+ true
71
+ rescue ArgumentError
72
+ false
73
+ end
74
+
75
+ def validate_datetime_format(value, format)
76
+ case format
77
+ when :iso8601, 'iso8601'
78
+ iso_datetime_pattern = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/
79
+ iso_datetime_pattern.match?(value) ? [] : ['DateTime must be in ISO8601 format']
80
+ when :rfc3339, 'rfc3339'
81
+ # RFC3339 is essentially the same as ISO8601 for our purposes
82
+ iso_datetime_pattern = /\A\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})\z/
83
+ iso_datetime_pattern.match?(value) ? [] : ['DateTime must be in RFC3339 format']
84
+ when String
85
+ # Custom format validation
86
+ begin
87
+ ::DateTime.strptime(value, format)
88
+ []
89
+ rescue ArgumentError
90
+ ["DateTime does not match format #{format}"]
91
+ end
92
+ else
93
+ []
94
+ end
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'string'
4
+
5
+ module RapiTapir
6
+ module Types
7
+ # Email type for validating email address strings
8
+ # Extends String type with email format validation
9
+ class Email < String
10
+ EMAIL_PATTERN = /\A[\w+\-.]+@[a-z\d-]+(\.[a-z\d-]+)*\.[a-z]+\z/i
11
+
12
+ def initialize(**options)
13
+ super(pattern: EMAIL_PATTERN, format: :email, **options)
14
+ end
15
+
16
+ protected
17
+
18
+ def validate_type(value)
19
+ return ["Expected string, got #{value.class}"] unless value.is_a?(::String)
20
+ return ['Invalid email format'] unless EMAIL_PATTERN.match?(value)
21
+
22
+ []
23
+ end
24
+
25
+ def apply_constraints_to_schema(schema)
26
+ super
27
+ schema[:format] = 'email'
28
+ schema[:pattern] = EMAIL_PATTERN.source
29
+ end
30
+ end
31
+ end
32
+ end