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