rapitapir 0.1.2 → 2.0.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 (109) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +7 -7
  3. data/.rubocop_todo.yml +83 -0
  4. data/README.md +1319 -235
  5. data/RUBY_WEEKLY_LAUNCH_POST.md +219 -0
  6. data/docs/RAILS_INTEGRATION_IMPLEMENTATION.md +209 -0
  7. data/docs/SINATRA_EXTENSION.md +399 -348
  8. data/docs/STRICT_VALIDATION.md +229 -0
  9. data/docs/VALIDATION_IMPROVEMENTS.md +218 -0
  10. data/docs/ai-integration-plan.md +112 -0
  11. data/docs/auto-derivation.md +505 -92
  12. data/docs/endpoint-definition.md +536 -129
  13. data/docs/n8n-integration.md +212 -0
  14. data/docs/observability.md +810 -500
  15. data/docs/using-mcp.md +93 -0
  16. data/examples/ai/knowledge_base_rag.rb +83 -0
  17. data/examples/ai/user_management_mcp.rb +92 -0
  18. data/examples/ai/user_validation_llm.rb +187 -0
  19. data/examples/rails/RAILS_8_GUIDE.md +165 -0
  20. data/examples/rails/RAILS_LOADING_FIX.rb +35 -0
  21. data/examples/rails/README.md +497 -0
  22. data/examples/rails/comprehensive_test.rb +91 -0
  23. data/examples/rails/config/routes.rb +48 -0
  24. data/examples/rails/debug_controller.rb +63 -0
  25. data/examples/rails/detailed_test.rb +46 -0
  26. data/examples/rails/enhanced_users_controller.rb +278 -0
  27. data/examples/rails/final_server_test.rb +50 -0
  28. data/examples/rails/hello_world_app.rb +116 -0
  29. data/examples/rails/hello_world_controller.rb +186 -0
  30. data/examples/rails/hello_world_routes.rb +28 -0
  31. data/examples/rails/rails8_minimal_demo.rb +132 -0
  32. data/examples/rails/rails8_simple_demo.rb +140 -0
  33. data/examples/rails/rails8_working_demo.rb +255 -0
  34. data/examples/rails/real_world_blog_api.rb +510 -0
  35. data/examples/rails/server_test.rb +46 -0
  36. data/examples/rails/test_direct_processing.rb +41 -0
  37. data/examples/rails/test_hello_world.rb +80 -0
  38. data/examples/rails/test_rails_integration.rb +54 -0
  39. data/examples/rails/traditional_app/Gemfile +37 -0
  40. data/examples/rails/traditional_app/README.md +265 -0
  41. data/examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb +254 -0
  42. data/examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb +220 -0
  43. data/examples/rails/traditional_app/app/controllers/application_controller.rb +86 -0
  44. data/examples/rails/traditional_app/app/controllers/application_controller_simplified.rb +87 -0
  45. data/examples/rails/traditional_app/app/controllers/documentation_controller.rb +149 -0
  46. data/examples/rails/traditional_app/app/controllers/health_controller.rb +42 -0
  47. data/examples/rails/traditional_app/config/routes.rb +25 -0
  48. data/examples/rails/traditional_app/config/routes_best_practice.rb +25 -0
  49. data/examples/rails/traditional_app/config/routes_simplified.rb +36 -0
  50. data/examples/rails/traditional_app_runnable.rb +406 -0
  51. data/examples/rails/users_controller.rb +4 -1
  52. data/examples/serverless/Gemfile +43 -0
  53. data/examples/serverless/QUICKSTART.md +331 -0
  54. data/examples/serverless/README.md +520 -0
  55. data/examples/serverless/aws_lambda_example.rb +307 -0
  56. data/examples/serverless/aws_sam_template.yaml +215 -0
  57. data/examples/serverless/azure_functions_example.rb +407 -0
  58. data/examples/serverless/deploy.rb +204 -0
  59. data/examples/serverless/gcp_cloud_functions_example.rb +367 -0
  60. data/examples/serverless/gcp_function.yaml +23 -0
  61. data/examples/serverless/host.json +24 -0
  62. data/examples/serverless/package.json +32 -0
  63. data/examples/serverless/spec/aws_lambda_spec.rb +196 -0
  64. data/examples/serverless/spec/spec_helper.rb +89 -0
  65. data/examples/serverless/vercel.json +31 -0
  66. data/examples/serverless/vercel_example.rb +404 -0
  67. data/examples/strict_validation_examples.rb +104 -0
  68. data/examples/validation_error_examples.rb +173 -0
  69. data/lib/rapitapir/ai/llm_instruction.rb +456 -0
  70. data/lib/rapitapir/ai/mcp.rb +134 -0
  71. data/lib/rapitapir/ai/rag.rb +287 -0
  72. data/lib/rapitapir/ai/rag_middleware.rb +147 -0
  73. data/lib/rapitapir/auth/oauth2.rb +43 -57
  74. data/lib/rapitapir/cli/command.rb +362 -2
  75. data/lib/rapitapir/cli/mcp_export.rb +18 -0
  76. data/lib/rapitapir/cli/validator.rb +2 -6
  77. data/lib/rapitapir/core/endpoint.rb +59 -6
  78. data/lib/rapitapir/core/enhanced_endpoint.rb +2 -6
  79. data/lib/rapitapir/dsl/fluent_endpoint_builder.rb +53 -0
  80. data/lib/rapitapir/endpoint_registry.rb +47 -0
  81. data/lib/rapitapir/observability/health_check.rb +4 -4
  82. data/lib/rapitapir/observability/logging.rb +10 -10
  83. data/lib/rapitapir/schema.rb +2 -2
  84. data/lib/rapitapir/server/rack_adapter.rb +1 -3
  85. data/lib/rapitapir/server/rails/configuration.rb +77 -0
  86. data/lib/rapitapir/server/rails/controller_base.rb +185 -0
  87. data/lib/rapitapir/server/rails/documentation_helpers.rb +76 -0
  88. data/lib/rapitapir/server/rails/resource_builder.rb +181 -0
  89. data/lib/rapitapir/server/rails/routes.rb +114 -0
  90. data/lib/rapitapir/server/rails_adapter.rb +10 -3
  91. data/lib/rapitapir/server/rails_adapter_class.rb +1 -3
  92. data/lib/rapitapir/server/rails_controller.rb +1 -3
  93. data/lib/rapitapir/server/rails_integration.rb +67 -0
  94. data/lib/rapitapir/server/rails_response_handler.rb +16 -3
  95. data/lib/rapitapir/server/sinatra_adapter.rb +29 -5
  96. data/lib/rapitapir/server/sinatra_integration.rb +4 -4
  97. data/lib/rapitapir/sinatra/extension.rb +2 -2
  98. data/lib/rapitapir/sinatra/oauth2_helpers.rb +34 -40
  99. data/lib/rapitapir/types/array.rb +4 -0
  100. data/lib/rapitapir/types/auto_derivation.rb +4 -18
  101. data/lib/rapitapir/types/datetime.rb +1 -3
  102. data/lib/rapitapir/types/float.rb +2 -6
  103. data/lib/rapitapir/types/hash.rb +40 -2
  104. data/lib/rapitapir/types/integer.rb +4 -12
  105. data/lib/rapitapir/types/object.rb +6 -2
  106. data/lib/rapitapir/types.rb +6 -2
  107. data/lib/rapitapir/version.rb +1 -1
  108. data/lib/rapitapir.rb +5 -3
  109. metadata +74 -2
@@ -10,37 +10,37 @@ module RapiTapir
10
10
  # Configure Auth0 OAuth2 authentication
11
11
  def auth0_oauth2(scheme_name = :oauth2_auth0, domain:, audience:, **options)
12
12
  auth_scheme = RapiTapir::Auth::OAuth2::Auth0Scheme.new(scheme_name, {
13
- domain: domain,
14
- audience: audience,
15
- **options
16
- })
17
-
13
+ domain: domain,
14
+ audience: audience,
15
+ **options
16
+ })
17
+
18
18
  # Store the auth scheme for use in endpoint protection
19
19
  settings.rapitapir_config.add_auth_scheme(scheme_name, auth_scheme)
20
-
20
+
21
21
  # Add authentication helper methods
22
22
  helpers do
23
23
  include OAuth2HelperMethods
24
24
  end
25
-
25
+
26
26
  auth_scheme
27
27
  end
28
28
 
29
29
  # Configure generic OAuth2 authentication with token introspection
30
30
  def oauth2_introspection(scheme_name = :oauth2, introspection_endpoint:, client_id:, client_secret:, **options)
31
31
  auth_scheme = RapiTapir::Auth::OAuth2::GenericScheme.new(scheme_name, {
32
- introspection_endpoint: introspection_endpoint,
33
- client_id: client_id,
34
- client_secret: client_secret,
35
- **options
36
- })
37
-
32
+ introspection_endpoint: introspection_endpoint,
33
+ client_id: client_id,
34
+ client_secret: client_secret,
35
+ **options
36
+ })
37
+
38
38
  settings.rapitapir_config.add_auth_scheme(scheme_name, auth_scheme)
39
-
39
+
40
40
  helpers do
41
41
  include OAuth2HelperMethods
42
42
  end
43
-
43
+
44
44
  auth_scheme
45
45
  end
46
46
 
@@ -58,7 +58,7 @@ module RapiTapir
58
58
  before do
59
59
  # Skip protection for excluded paths
60
60
  next if except.any? { |pattern| request.path_info.match?(pattern) }
61
-
61
+
62
62
  authorize_oauth2!(required_scopes: scopes, scheme: scheme)
63
63
  end
64
64
  end
@@ -69,20 +69,18 @@ module RapiTapir
69
69
  # Authenticate and authorize with OAuth2
70
70
  def authorize_oauth2!(required_scopes: [], scheme: :oauth2_auth0)
71
71
  auth_scheme = settings.rapitapir_config.auth_schemes[scheme]
72
-
73
- unless auth_scheme
74
- halt 500, { error: 'OAuth2 authentication not configured' }.to_json
75
- end
72
+
73
+ halt 500, { error: 'OAuth2 authentication not configured' }.to_json unless auth_scheme
76
74
 
77
75
  begin
78
76
  context = auth_scheme.authenticate(request)
79
-
77
+
80
78
  unless context
81
79
  challenge = auth_scheme.challenge
82
80
  headers 'WWW-Authenticate' => challenge
83
- halt 401, {
81
+ halt 401, {
84
82
  error: 'unauthorized',
85
- error_description: 'Access token required'
83
+ error_description: 'Access token required'
86
84
  }.to_json
87
85
  end
88
86
 
@@ -92,7 +90,7 @@ module RapiTapir
92
90
  # Check required scopes if specified
93
91
  if required_scopes.any?
94
92
  missing_scopes = required_scopes - context.scopes
95
-
93
+
96
94
  if missing_scopes.any?
97
95
  halt 403, {
98
96
  error: 'insufficient_scope',
@@ -135,34 +133,34 @@ module RapiTapir
135
133
  # Check if user has specific scope
136
134
  def has_scope?(scope)
137
135
  return false unless current_auth_context
138
-
136
+
139
137
  current_auth_context.scopes.include?(scope.to_s)
140
138
  end
141
139
 
142
140
  # Check if user has all required scopes
143
141
  def has_scopes?(*scopes)
144
142
  return false unless current_auth_context
145
-
143
+
146
144
  scopes.all? { |scope| has_scope?(scope) }
147
145
  end
148
146
 
149
147
  # Check if user has any of the specified scopes
150
148
  def has_any_scope?(*scopes)
151
149
  return false unless current_auth_context
152
-
150
+
153
151
  scopes.any? { |scope| has_scope?(scope) }
154
152
  end
155
153
 
156
154
  # Require specific scopes for the current request
157
155
  def require_scopes!(*scopes)
158
156
  missing_scopes = scopes.reject { |scope| has_scope?(scope) }
159
-
160
- if missing_scopes.any?
161
- halt 403, {
162
- error: 'insufficient_scope',
163
- error_description: "Missing required scopes: #{missing_scopes.join(', ')}"
164
- }.to_json
165
- end
157
+
158
+ return unless missing_scopes.any?
159
+
160
+ halt 403, {
161
+ error: 'insufficient_scope',
162
+ error_description: "Missing required scopes: #{missing_scopes.join(', ')}"
163
+ }.to_json
166
164
  end
167
165
 
168
166
  # Extract token from Authorization header
@@ -226,11 +224,7 @@ module RapiTapir
226
224
  end
227
225
 
228
226
  # Extend the Sinatra extension with OAuth2 helpers if it exists
229
- if defined?(RapiTapir::Sinatra::Extension)
230
- RapiTapir::Sinatra::Extension::ClassMethods.include(RapiTapir::Sinatra::OAuth2Helpers)
231
- end
227
+ RapiTapir::Sinatra::Extension::ClassMethods.include(RapiTapir::Sinatra::OAuth2Helpers) if defined?(RapiTapir::Sinatra::Extension)
232
228
 
233
229
  # Extend endpoint building with OAuth2 methods
234
- if defined?(RapiTapir::DSL::FluentEndpointBuilder)
235
- RapiTapir::DSL::FluentEndpointBuilder.include(RapiTapir::Sinatra::OAuth2EndpointExtensions)
236
- end
230
+ RapiTapir::DSL::FluentEndpointBuilder.include(RapiTapir::Sinatra::OAuth2EndpointExtensions) if defined?(RapiTapir::DSL::FluentEndpointBuilder)
@@ -136,12 +136,16 @@ module RapiTapir
136
136
  schema[:uniqueItems] = constraints[:unique_items] if constraints[:unique_items]
137
137
  end
138
138
 
139
+ public
140
+
139
141
  def to_s
140
142
  item_type_str = format_item_type_string
141
143
  constraint_part = format_constraints_string
142
144
  "Array[#{item_type_str}]#{constraint_part}"
143
145
  end
144
146
 
147
+ private
148
+
145
149
  def format_item_type_string
146
150
  item_type.respond_to?(:to_s) ? item_type.to_s : item_type.class.name
147
151
  end
@@ -22,7 +22,6 @@ module RapiTapir
22
22
  # @param only [Array<Symbol>] Only include these fields
23
23
  # @param except [Array<Symbol>] Exclude these fields
24
24
  # @return [RapiTapir::Types::Hash]
25
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
26
25
  def from_hash(hash, only: nil, except: nil)
27
26
  raise ArgumentError, "Expected Hash, got #{hash.class}" unless hash.is_a?(::Hash)
28
27
 
@@ -39,7 +38,6 @@ module RapiTapir
39
38
 
40
39
  create_hash_type(schema_hash)
41
40
  end
42
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
43
41
 
44
42
  # Derive schema from JSON Schema object
45
43
  #
@@ -47,11 +45,8 @@ module RapiTapir
47
45
  # @param only [Array<Symbol>] Only include these fields
48
46
  # @param except [Array<Symbol>] Exclude these fields
49
47
  # @return [RapiTapir::Types::Hash]
50
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
51
48
  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
49
+ raise ArgumentError, 'JSON Schema must be an object type' unless json_schema.is_a?(::Hash) && json_schema['type'] == 'object'
55
50
 
56
51
  properties = json_schema['properties'] || {}
57
52
  required_fields = Array(json_schema['required'])
@@ -69,7 +64,6 @@ module RapiTapir
69
64
 
70
65
  RapiTapir::Types.hash(schema_hash)
71
66
  end
72
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
73
67
 
74
68
  # Derive schema from OpenStruct instance
75
69
  #
@@ -77,7 +71,7 @@ module RapiTapir
77
71
  # @param only [Array<Symbol>] Only include these fields
78
72
  # @param except [Array<Symbol>] Exclude these fields
79
73
  # @return [RapiTapir::Types::Hash]
80
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Style/OpenStructUse
74
+ # rubocop:disable Style/OpenStructUse
81
75
  def from_open_struct(open_struct, only: nil, except: nil)
82
76
  raise ArgumentError, "Expected OpenStruct, got #{open_struct.class}" unless open_struct.is_a?(OpenStruct)
83
77
 
@@ -93,7 +87,7 @@ module RapiTapir
93
87
 
94
88
  create_hash_type(schema_hash)
95
89
  end
96
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity, Style/OpenStructUse
90
+ # rubocop:enable Style/OpenStructUse
97
91
 
98
92
  # Derive schema from Protobuf message class
99
93
  #
@@ -101,11 +95,8 @@ module RapiTapir
101
95
  # @param only [Array<Symbol>] Only include these fields
102
96
  # @param except [Array<Symbol>] Exclude these fields
103
97
  # @return [RapiTapir::Types::Hash]
104
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
105
98
  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
99
+ raise ArgumentError, 'Protobuf not available or invalid protobuf class' unless defined?(Google::Protobuf) && proto_class.respond_to?(:descriptor)
109
100
 
110
101
  schema_hash = {}
111
102
  proto_class.descriptor.each do |field_descriptor|
@@ -121,12 +112,10 @@ module RapiTapir
121
112
 
122
113
  create_hash_type(schema_hash)
123
114
  end
124
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
125
115
 
126
116
  private
127
117
 
128
118
  # Convert JSON Schema field type to RapiTapir type
129
- # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity
130
119
  def convert_json_schema_type(field_schema, required: true)
131
120
  base_type = case field_schema['type']
132
121
  when 'string'
@@ -163,7 +152,6 @@ module RapiTapir
163
152
 
164
153
  required ? base_type : create_optional_type(base_type)
165
154
  end
166
- # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity
167
155
 
168
156
  # Convert protobuf field descriptor to RapiTapir type
169
157
  def convert_protobuf_type(field_descriptor)
@@ -189,7 +177,6 @@ module RapiTapir
189
177
  end
190
178
 
191
179
  # Infer RapiTapir type from Ruby value
192
- # rubocop:disable Metrics/CyclomaticComplexity
193
180
  def infer_type_from_value(value)
194
181
  case value
195
182
  when ::Integer
@@ -212,7 +199,6 @@ module RapiTapir
212
199
  create_string_type
213
200
  end
214
201
  end
215
- # rubocop:enable Metrics/CyclomaticComplexity
216
202
 
217
203
  # Helper methods to create types without using convenience methods
218
204
  def create_string_type
@@ -42,9 +42,7 @@ module RapiTapir
42
42
  # Assume Unix timestamp
43
43
  ::Time.at(value).to_datetime
44
44
  else
45
- unless value.respond_to?(:to_datetime)
46
- raise CoercionError.new(value, 'DateTime', 'Value cannot be converted to DateTime')
47
- end
45
+ raise CoercionError.new(value, 'DateTime', 'Value cannot be converted to DateTime') unless value.respond_to?(:to_datetime)
48
46
 
49
47
  value.to_datetime
50
48
 
@@ -39,9 +39,7 @@ module RapiTapir
39
39
  def validate_minimum_constraints(float_value)
40
40
  errors = []
41
41
 
42
- if constraints[:minimum] && float_value < constraints[:minimum]
43
- errors << "Value #{float_value} is below minimum #{constraints[:minimum]}"
44
- end
42
+ errors << "Value #{float_value} is below minimum #{constraints[:minimum]}" if constraints[:minimum] && float_value < constraints[:minimum]
45
43
 
46
44
  if constraints[:exclusive_minimum] && float_value <= constraints[:exclusive_minimum]
47
45
  errors << "Value #{float_value} must be greater than #{constraints[:exclusive_minimum]}"
@@ -53,9 +51,7 @@ module RapiTapir
53
51
  def validate_maximum_constraints(float_value)
54
52
  errors = []
55
53
 
56
- if constraints[:maximum] && float_value > constraints[:maximum]
57
- errors << "Value #{float_value} exceeds maximum #{constraints[:maximum]}"
58
- end
54
+ errors << "Value #{float_value} exceeds maximum #{constraints[:maximum]}" if constraints[:maximum] && float_value > constraints[:maximum]
59
55
 
60
56
  if constraints[:exclusive_maximum] && float_value >= constraints[:exclusive_maximum]
61
57
  errors << "Value #{float_value} must be less than #{constraints[:exclusive_maximum]}"
@@ -9,7 +9,7 @@ module RapiTapir
9
9
  class Hash < Base
10
10
  attr_reader :field_types
11
11
 
12
- def initialize(field_types = {}, additional_properties: true, **options)
12
+ def initialize(field_types = {}, additional_properties: false, **options)
13
13
  @field_types = field_types.freeze
14
14
  super(
15
15
  additional_properties: additional_properties,
@@ -39,6 +39,16 @@ module RapiTapir
39
39
 
40
40
  field_types.each do |field_name, field_type|
41
41
  field_value = extract_field_value(value, field_name)
42
+
43
+ # Check for missing required fields
44
+ if field_value.nil? && !field_type.optional?
45
+ errors << "Required field '#{field_name}' is missing"
46
+ next
47
+ end
48
+
49
+ # Skip validation for optional missing fields
50
+ next if field_value.nil? && field_type.optional?
51
+
42
52
  field_result = field_type.validate(field_value)
43
53
  next if field_result[:valid]
44
54
 
@@ -82,6 +92,9 @@ module RapiTapir
82
92
  def coerce_hash_value(value)
83
93
  coerced = {}
84
94
 
95
+ # Check for unexpected fields if additional properties are not allowed
96
+ validate_no_unexpected_fields(value) unless constraints[:additional_properties]
97
+
85
98
  # Coerce defined fields
86
99
  coerce_defined_fields(value, coerced)
87
100
 
@@ -91,10 +104,33 @@ module RapiTapir
91
104
  coerced
92
105
  end
93
106
 
107
+ def validate_no_unexpected_fields(value)
108
+ expected_keys = field_types.keys.map { |k| [k, k.to_s, k.to_sym] }.flatten.uniq
109
+ unexpected_keys = value.keys - expected_keys
110
+ return if unexpected_keys.empty?
111
+
112
+ unexpected_list = unexpected_keys.map(&:inspect).join(', ')
113
+ raise CoercionError.new(value, 'Hash', "Unexpected fields in hash: #{unexpected_list}. Only these fields are allowed: #{field_types.keys.join(', ')}")
114
+ end
115
+
94
116
  def coerce_defined_fields(value, coerced)
95
117
  field_types.each do |field_name, field_type|
96
118
  field_value = find_field_value(value, field_name)
97
- coerced[field_name] = field_type.coerce(field_value) if field_value || !field_type.optional?
119
+
120
+ # Check for missing required fields
121
+ if field_value.nil? && !field_type.optional?
122
+ raise CoercionError.new(nil, field_type.class.name, "Required field '#{field_name}' is missing from hash")
123
+ end
124
+
125
+ # Only coerce if we have a value or field is optional
126
+ next unless field_value || !field_type.optional?
127
+
128
+ begin
129
+ coerced[field_name] = field_type.coerce(field_value)
130
+ rescue CoercionError => e
131
+ # Re-raise with field context
132
+ raise CoercionError.new(e.value, e.type, "Field '#{field_name}': #{e.reason}")
133
+ end
98
134
  end
99
135
  end
100
136
 
@@ -148,6 +184,8 @@ module RapiTapir
148
184
  schema[:additionalProperties] = constraints[:additional_properties]
149
185
  end
150
186
 
187
+ public
188
+
151
189
  def to_s
152
190
  if field_types.empty?
153
191
  'Hash'
@@ -46,9 +46,7 @@ module RapiTapir
46
46
  def validate_minimum_constraints(value)
47
47
  errors = []
48
48
 
49
- if constraints[:minimum] && value < constraints[:minimum]
50
- errors << "Value #{value} is below minimum #{constraints[:minimum]}"
51
- end
49
+ errors << "Value #{value} is below minimum #{constraints[:minimum]}" if constraints[:minimum] && value < constraints[:minimum]
52
50
 
53
51
  if constraints[:exclusive_minimum] && value <= constraints[:exclusive_minimum]
54
52
  errors << "Value #{value} must be greater than #{constraints[:exclusive_minimum]}"
@@ -60,9 +58,7 @@ module RapiTapir
60
58
  def validate_maximum_constraints(value)
61
59
  errors = []
62
60
 
63
- if constraints[:maximum] && value > constraints[:maximum]
64
- errors << "Value #{value} exceeds maximum #{constraints[:maximum]}"
65
- end
61
+ errors << "Value #{value} exceeds maximum #{constraints[:maximum]}" if constraints[:maximum] && value > constraints[:maximum]
66
62
 
67
63
  if constraints[:exclusive_maximum] && value >= constraints[:exclusive_maximum]
68
64
  errors << "Value #{value} must be less than #{constraints[:exclusive_maximum]}"
@@ -74,9 +70,7 @@ module RapiTapir
74
70
  def validate_multiple_constraint(value)
75
71
  errors = []
76
72
 
77
- if constraints[:multiple_of] && (value % constraints[:multiple_of]) != 0
78
- errors << "Value #{value} is not a multiple of #{constraints[:multiple_of]}"
79
- end
73
+ errors << "Value #{value} is not a multiple of #{constraints[:multiple_of]}" if constraints[:multiple_of] && (value % constraints[:multiple_of]) != 0
80
74
 
81
75
  errors
82
76
  end
@@ -103,9 +97,7 @@ module RapiTapir
103
97
  end
104
98
 
105
99
  def coerce_other_to_integer(value)
106
- unless value.respond_to?(:to_i)
107
- raise CoercionError.new(value, 'Integer', 'Value cannot be converted to integer')
108
- end
100
+ raise CoercionError.new(value, 'Integer', 'Value cannot be converted to integer') unless value.respond_to?(:to_i)
109
101
 
110
102
  value.to_i
111
103
  end
@@ -9,14 +9,14 @@ module RapiTapir
9
9
  class Object < Base
10
10
  attr_reader :fields
11
11
 
12
- def initialize(**options, &block)
12
+ def initialize(**options, &)
13
13
  @fields = {}
14
14
  super(**options)
15
15
 
16
16
  return unless block_given?
17
17
 
18
18
  builder = ObjectBuilder.new(self)
19
- builder.instance_eval(&block)
19
+ builder.instance_eval(&)
20
20
  end
21
21
 
22
22
  def field(name, type, required: true, **options)
@@ -118,6 +118,8 @@ module RapiTapir
118
118
  schema[:additionalProperties] = false # Objects are strict by default
119
119
  end
120
120
 
121
+ public
122
+
121
123
  def to_s
122
124
  if fields.empty?
123
125
  'Object'
@@ -127,6 +129,8 @@ module RapiTapir
127
129
  end
128
130
  end
129
131
 
132
+ private
133
+
130
134
  def extract_field_value(hash, field_name)
131
135
  # Try different key formats: symbol, string, and string version of symbol
132
136
  hash[field_name] || hash[field_name.to_s] || hash[field_name.to_sym]
@@ -124,12 +124,16 @@ module RapiTapir
124
124
  Hash.new(field_types, **options)
125
125
  end
126
126
 
127
+ def self.open_hash(field_types = {}, **options)
128
+ Hash.new(field_types, additional_properties: true, **options)
129
+ end
130
+
127
131
  def self.optional(type)
128
132
  Optional.new(type)
129
133
  end
130
134
 
131
- def self.object(&block)
132
- Object.new(&block)
135
+ def self.object(&)
136
+ Object.new(&)
133
137
  end
134
138
 
135
139
  # Auto-derivation convenience methods
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RapiTapir
4
- VERSION = '0.1.2'
4
+ VERSION = '2.0.0'
5
5
  end
data/lib/rapitapir.rb CHANGED
@@ -43,10 +43,12 @@ rescue LoadError
43
43
  # Sinatra not available
44
44
  end
45
45
 
46
+ # Rails integration (load unconditionally since Rails apps will define Rails)
46
47
  begin
47
- require_relative 'rapitapir/server/rails_adapter' if defined?(Rails)
48
- rescue LoadError
49
- # Rails not available
48
+ require_relative 'rapitapir/server/rails_integration'
49
+ rescue LoadError => e
50
+ # Rails integration files not available
51
+ warn "Rails integration not available: #{e.message}" if $DEBUG
50
52
  end
51
53
 
52
54
  # OpenAPI and client generation (optional)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rapitapir
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Riccardo Merolla
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2025-08-04 00:00:00.000000000 Z
11
+ date: 2025-08-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: json
@@ -230,14 +230,20 @@ extra_rdoc_files: []
230
230
  files:
231
231
  - ".rspec"
232
232
  - ".rubocop.yml"
233
+ - ".rubocop_todo.yml"
233
234
  - CHANGELOG.md
234
235
  - CLEANUP_SUMMARY.md
235
236
  - CONTRIBUTING.md
236
237
  - LICENSE
237
238
  - README.md
239
+ - RUBY_WEEKLY_LAUNCH_POST.md
238
240
  - debug_hash.rb
239
241
  - docs/EXTENSION_COMPARISON.md
242
+ - docs/RAILS_INTEGRATION_IMPLEMENTATION.md
240
243
  - docs/SINATRA_EXTENSION.md
244
+ - docs/STRICT_VALIDATION.md
245
+ - docs/VALIDATION_IMPROVEMENTS.md
246
+ - docs/ai-integration-plan.md
241
247
  - docs/archive/PHASE_1_2_COMPLETE.md
242
248
  - docs/archive/PHASE_1_3_COMPLETE.md
243
249
  - docs/archive/PHASE_2_1_OBSERVABILITY_COMPLETED.md
@@ -253,11 +259,16 @@ files:
253
259
  - docs/github_pages_fix.md
254
260
  - docs/github_pages_setup.md
255
261
  - docs/implementation-status.md
262
+ - docs/n8n-integration.md
256
263
  - docs/observability.md
257
264
  - docs/phase3-plan.md
258
265
  - docs/sinatra_rapitapir.md
259
266
  - docs/type_shortcuts.md
267
+ - docs/using-mcp.md
260
268
  - examples/README_ENTERPRISE.md
269
+ - examples/ai/knowledge_base_rag.rb
270
+ - examples/ai/user_management_mcp.rb
271
+ - examples/ai/user_validation_llm.rb
261
272
  - examples/authentication_example.rb
262
273
  - examples/auto_derivation_ruby_friendly.rb
263
274
  - examples/cli/user_api_endpoints.rb
@@ -297,17 +308,70 @@ files:
297
308
  - examples/observability/test_working_validation.rb
298
309
  - examples/openapi/user_api_schema.rb
299
310
  - examples/production_ready_example.rb
311
+ - examples/rails/RAILS_8_GUIDE.md
312
+ - examples/rails/RAILS_LOADING_FIX.rb
313
+ - examples/rails/README.md
314
+ - examples/rails/comprehensive_test.rb
315
+ - examples/rails/config/routes.rb
316
+ - examples/rails/debug_controller.rb
317
+ - examples/rails/detailed_test.rb
318
+ - examples/rails/enhanced_users_controller.rb
319
+ - examples/rails/final_server_test.rb
320
+ - examples/rails/hello_world_app.rb
321
+ - examples/rails/hello_world_controller.rb
322
+ - examples/rails/hello_world_routes.rb
323
+ - examples/rails/rails8_minimal_demo.rb
324
+ - examples/rails/rails8_simple_demo.rb
325
+ - examples/rails/rails8_working_demo.rb
326
+ - examples/rails/real_world_blog_api.rb
327
+ - examples/rails/server_test.rb
328
+ - examples/rails/test_direct_processing.rb
329
+ - examples/rails/test_hello_world.rb
330
+ - examples/rails/test_rails_integration.rb
331
+ - examples/rails/traditional_app/Gemfile
332
+ - examples/rails/traditional_app/README.md
333
+ - examples/rails/traditional_app/app/controllers/api/v1/posts_controller.rb
334
+ - examples/rails/traditional_app/app/controllers/api/v1/users_controller.rb
335
+ - examples/rails/traditional_app/app/controllers/application_controller.rb
336
+ - examples/rails/traditional_app/app/controllers/application_controller_simplified.rb
337
+ - examples/rails/traditional_app/app/controllers/documentation_controller.rb
338
+ - examples/rails/traditional_app/app/controllers/health_controller.rb
339
+ - examples/rails/traditional_app/config/routes.rb
340
+ - examples/rails/traditional_app/config/routes_best_practice.rb
341
+ - examples/rails/traditional_app/config/routes_simplified.rb
342
+ - examples/rails/traditional_app_runnable.rb
300
343
  - examples/rails/users_controller.rb
301
344
  - examples/readme/basic_sinatra_example.rb
302
345
  - examples/server/user_api.rb
346
+ - examples/serverless/Gemfile
347
+ - examples/serverless/QUICKSTART.md
348
+ - examples/serverless/README.md
349
+ - examples/serverless/aws_lambda_example.rb
350
+ - examples/serverless/aws_sam_template.yaml
351
+ - examples/serverless/azure_functions_example.rb
352
+ - examples/serverless/deploy.rb
353
+ - examples/serverless/gcp_cloud_functions_example.rb
354
+ - examples/serverless/gcp_function.yaml
355
+ - examples/serverless/host.json
356
+ - examples/serverless/package.json
357
+ - examples/serverless/spec/aws_lambda_spec.rb
358
+ - examples/serverless/spec/spec_helper.rb
359
+ - examples/serverless/vercel.json
360
+ - examples/serverless/vercel_example.rb
303
361
  - examples/simple_auto_derivation_demo.rb
304
362
  - examples/simple_demo_api.rb
305
363
  - examples/sinatra/user_app.rb
364
+ - examples/strict_validation_examples.rb
306
365
  - examples/t_shortcut_demo.rb
307
366
  - examples/user_api.rb
367
+ - examples/validation_error_examples.rb
308
368
  - examples/working_getting_started.rb
309
369
  - examples/working_simple_example.rb
310
370
  - lib/rapitapir.rb
371
+ - lib/rapitapir/ai/llm_instruction.rb
372
+ - lib/rapitapir/ai/mcp.rb
373
+ - lib/rapitapir/ai/rag.rb
374
+ - lib/rapitapir/ai/rag_middleware.rb
311
375
  - lib/rapitapir/auth.rb
312
376
  - lib/rapitapir/auth/configuration.rb
313
377
  - lib/rapitapir/auth/context.rb
@@ -316,6 +380,7 @@ files:
316
380
  - lib/rapitapir/auth/oauth2.rb
317
381
  - lib/rapitapir/auth/schemes.rb
318
382
  - lib/rapitapir/cli/command.rb
383
+ - lib/rapitapir/cli/mcp_export.rb
319
384
  - lib/rapitapir/cli/server.rb
320
385
  - lib/rapitapir/cli/validator.rb
321
386
  - lib/rapitapir/client/generator_base.rb
@@ -340,6 +405,7 @@ files:
340
405
  - lib/rapitapir/dsl/observability_methods.rb
341
406
  - lib/rapitapir/dsl/output_methods.rb
342
407
  - lib/rapitapir/dsl/type_resolution.rb
408
+ - lib/rapitapir/endpoint_registry.rb
343
409
  - lib/rapitapir/observability.rb
344
410
  - lib/rapitapir/observability/configuration.rb
345
411
  - lib/rapitapir/observability/health_check.rb
@@ -353,10 +419,16 @@ files:
353
419
  - lib/rapitapir/server/middleware.rb
354
420
  - lib/rapitapir/server/path_matcher.rb
355
421
  - lib/rapitapir/server/rack_adapter.rb
422
+ - lib/rapitapir/server/rails/configuration.rb
423
+ - lib/rapitapir/server/rails/controller_base.rb
424
+ - lib/rapitapir/server/rails/documentation_helpers.rb
425
+ - lib/rapitapir/server/rails/resource_builder.rb
426
+ - lib/rapitapir/server/rails/routes.rb
356
427
  - lib/rapitapir/server/rails_adapter.rb
357
428
  - lib/rapitapir/server/rails_adapter_class.rb
358
429
  - lib/rapitapir/server/rails_controller.rb
359
430
  - lib/rapitapir/server/rails_input_processor.rb
431
+ - lib/rapitapir/server/rails_integration.rb
360
432
  - lib/rapitapir/server/rails_response_handler.rb
361
433
  - lib/rapitapir/server/sinatra_adapter.rb
362
434
  - lib/rapitapir/server/sinatra_integration.rb