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,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Sinatra
5
+ # Resource builder for creating RESTful API endpoints
6
+ # Follows Open/Closed Principle - extensible for new resource types
7
+ class ResourceBuilder
8
+ def initialize(app, base_path, schema, **options)
9
+ @app = app
10
+ @base_path = base_path.chomp('/')
11
+ @schema = schema
12
+ @options = options
13
+ @endpoints = []
14
+ end
15
+
16
+ # Enable standard CRUD operations
17
+ def crud(except: [], only: nil, **handlers, &block)
18
+ operations = only || %i[index show create update destroy]
19
+ operations -= except if except.any?
20
+
21
+ # If a block is given, evaluate it in the context of this ResourceBuilder
22
+ # This allows method calls like: index { BookStore.all }
23
+ if block_given?
24
+ instance_eval(&block)
25
+ else
26
+ # Legacy style with handlers hash
27
+ operations.each do |operation|
28
+ send(operation, &handlers[operation]) if respond_to?(operation, true)
29
+ end
30
+ end
31
+ end
32
+
33
+ # List all resources (GET /resources)
34
+ def index(**options, &handler)
35
+ handler ||= proc { [] }
36
+
37
+ endpoint = build_index_endpoint(options)
38
+ @app.endpoint(endpoint, &handler)
39
+ end
40
+
41
+ def build_index_endpoint(options)
42
+ RapiTapir.get(@base_path)
43
+ .summary(options[:summary] || "List all #{resource_name}")
44
+ .description(options[:description] || "Retrieve a list of #{resource_name}")
45
+ .then { |ep| add_pagination_params(ep) }
46
+ .ok(RapiTapir::Types.array(@schema))
47
+ .build
48
+ end
49
+
50
+ def add_pagination_params(endpoint)
51
+ endpoint.query(:limit, RapiTapir::Types.optional(RapiTapir::Types.integer),
52
+ description: 'Maximum number of results')
53
+ .query(:offset, RapiTapir::Types.optional(RapiTapir::Types.integer),
54
+ description: 'Number of results to skip')
55
+ end
56
+
57
+ # Get specific resource (GET /resources/:id)
58
+ def show(**options, &handler)
59
+ handler ||= proc { {} }
60
+
61
+ endpoint = RapiTapir.get("#{@base_path}/:id")
62
+ .summary(options[:summary] || "Get #{resource_name}")
63
+ .description(options[:description] || "Retrieve a specific #{resource_name} by ID")
64
+ .path_param(:id, RapiTapir::Types.integer, description: "#{resource_name.capitalize} ID")
65
+ .ok(@schema)
66
+ .build
67
+
68
+ @app.endpoint(endpoint, &handler)
69
+ end
70
+
71
+ # Create new resource (POST /resources)
72
+ def create(**options, &handler)
73
+ handler ||= proc { {} }
74
+
75
+ endpoint = RapiTapir.post(@base_path)
76
+ .summary(options[:summary] || "Create #{resource_name}")
77
+ .description(options[:description] || "Create a new #{resource_name}")
78
+ .json_body(@schema)
79
+ .created(@schema)
80
+ .build
81
+
82
+ @app.endpoint(endpoint, &handler)
83
+ end
84
+
85
+ # Update resource (PUT /resources/:id)
86
+ def update(**options, &handler)
87
+ handler ||= proc { {} }
88
+
89
+ endpoint = RapiTapir.put("#{@base_path}/:id")
90
+ .summary(options[:summary] || "Update #{resource_name}")
91
+ .description(options[:description] || "Update an existing #{resource_name}")
92
+ .path_param(:id, RapiTapir::Types.integer, description: "#{resource_name.capitalize} ID")
93
+ .json_body(@schema)
94
+ .ok(@schema)
95
+ .build
96
+
97
+ @app.endpoint(endpoint, &handler)
98
+ end
99
+
100
+ # Delete resource (DELETE /resources/:id)
101
+ def destroy(**options, &handler)
102
+ handler ||= proc { status 204 }
103
+
104
+ endpoint = RapiTapir.delete("#{@base_path}/:id")
105
+ .summary(options[:summary] || "Delete #{resource_name}")
106
+ .description(options[:description] || "Delete a #{resource_name}")
107
+ .path_param(:id, RapiTapir::Types.integer, description: "#{resource_name.capitalize} ID")
108
+ .no_content
109
+ .build
110
+
111
+ @app.endpoint(endpoint, &handler)
112
+ end
113
+
114
+ # Custom endpoint within the resource
115
+ def custom(method, path, summary: nil, configure: nil, **_options, &handler)
116
+ full_path = path.start_with?('/') ? path : "#{@base_path}/#{path}"
117
+
118
+ endpoint = RapiTapir.send(method, full_path)
119
+ endpoint = endpoint.summary(summary) if summary
120
+ endpoint = configure.call(endpoint) if configure
121
+
122
+ @app.endpoint(endpoint.build, &handler)
123
+ end
124
+
125
+ private
126
+
127
+ def resource_name
128
+ @resource_name ||= @base_path.split('/').last.singularize
129
+ end
130
+ end
131
+ end
132
+ end
133
+
134
+ # String extensions for pluralization/singularization
135
+ class String
136
+ def pluralize
137
+ case self
138
+ when /s$/ then self
139
+ when /y$/ then sub(/y$/, 'ies')
140
+ when /(ch|sh|x|z)$/ then "#{self}es"
141
+ else "#{self}s"
142
+ end
143
+ end
144
+
145
+ def singularize
146
+ case self
147
+ when /ies$/ then sub(/ies$/, 'y')
148
+ when /s$/ then chomp('s')
149
+ else self
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RapiTapir
4
+ module Sinatra
5
+ # Swagger UI HTML generator
6
+ # Follows Single Responsibility Principle - only generates UI HTML
7
+ class SwaggerUIGenerator
8
+ def initialize(openapi_path, api_info)
9
+ @openapi_path = openapi_path
10
+ @api_info = api_info
11
+ end
12
+
13
+ def generate
14
+ <<~HTML
15
+ <!DOCTYPE html>
16
+ <html lang="en">
17
+ <head>
18
+ <meta charset="UTF-8">
19
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
20
+ <title>#{@api_info[:title]} - API Documentation</title>
21
+ <link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui.css" />
22
+ <style>
23
+ #{custom_styles}
24
+ </style>
25
+ </head>
26
+ <body>
27
+ #{header_banner}
28
+ <div id="swagger-ui"></div>
29
+ <script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-bundle.js"></script>
30
+ <script src="https://unpkg.com/swagger-ui-dist@5.9.0/swagger-ui-standalone-preset.js"></script>
31
+ <script>
32
+ #{swagger_ui_config}
33
+ </script>
34
+ </body>
35
+ </html>
36
+ HTML
37
+ end
38
+
39
+ private
40
+
41
+ def custom_styles
42
+ <<~CSS
43
+ html {
44
+ box-sizing: border-box;
45
+ overflow: -moz-scrollbars-vertical;
46
+ overflow-y: scroll;
47
+ }
48
+ *, *:before, *:after {
49
+ box-sizing: inherit;
50
+ }
51
+ body {
52
+ margin: 0;
53
+ background: #fafafa;
54
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
55
+ }
56
+ .swagger-ui .topbar {
57
+ display: none;
58
+ }
59
+ .info-banner {
60
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
61
+ color: white;
62
+ padding: 30px 20px;
63
+ text-align: center;
64
+ margin-bottom: 20px;
65
+ box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
66
+ }
67
+ .info-banner h1 {
68
+ margin: 0 0 10px 0;
69
+ font-size: 28px;
70
+ font-weight: 600;
71
+ }
72
+ .info-banner p {
73
+ margin: 0;
74
+ opacity: 0.9;
75
+ font-size: 16px;
76
+ }
77
+ .info-banner .badge {
78
+ display: inline-block;
79
+ background: rgba(255, 255, 255, 0.2);
80
+ padding: 4px 12px;
81
+ border-radius: 20px;
82
+ font-size: 14px;
83
+ margin: 10px 5px 0 0;
84
+ }
85
+ .swagger-ui .scheme-container {
86
+ padding: 30px 0;
87
+ background: white;
88
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
89
+ }
90
+ CSS
91
+ end
92
+
93
+ def header_banner
94
+ <<~HTML
95
+ <div class="info-banner">
96
+ <h1>#{@api_info[:title]}</h1>
97
+ <p>#{@api_info[:description]}</p>
98
+ <div>
99
+ <span class="badge">v#{@api_info[:version]}</span>
100
+ <span class="badge">🚀 RapiTapir</span>
101
+ <span class="badge">⚡ Auto-generated</span>
102
+ </div>
103
+ </div>
104
+ HTML
105
+ end
106
+
107
+ def swagger_ui_config
108
+ <<~JS
109
+ window.onload = function() {
110
+ const ui = SwaggerUIBundle({
111
+ url: '#{@openapi_path}',
112
+ dom_id: '#swagger-ui',
113
+ deepLinking: true,
114
+ presets: [
115
+ SwaggerUIBundle.presets.apis,
116
+ SwaggerUIStandalonePreset
117
+ ],
118
+ plugins: [
119
+ SwaggerUIBundle.plugins.DownloadUrl
120
+ ],
121
+ layout: "StandaloneLayout",
122
+ tryItOutEnabled: true,
123
+ supportedSubmitMethods: ['get', 'post', 'put', 'delete', 'patch', 'head', 'options'],
124
+ validatorUrl: null,
125
+ docExpansion: 'list',
126
+ filter: true,
127
+ showExtensions: true,
128
+ showCommonExtensions: true,
129
+ defaultModelsExpandDepth: 2,
130
+ defaultModelExpandDepth: 2,
131
+ displayRequestDuration: true,
132
+ requestInterceptor: function(request) {
133
+ // Add custom request headers or modify requests here
134
+ return request;
135
+ },
136
+ responseInterceptor: function(response) {
137
+ // Handle responses here
138
+ return response;
139
+ },
140
+ onComplete: function() {
141
+ console.log('🚀 RapiTapir Swagger UI loaded successfully');
142
+ console.log('📋 OpenAPI spec auto-generated from RapiTapir endpoints');
143
+ console.log('🔧 Powered by RapiTapir Sinatra Extension');
144
+ },
145
+ onFailure: function(error) {
146
+ console.error('Failed to load Swagger UI:', error);
147
+ }
148
+ });
149
+
150
+ // Custom enhancements
151
+ setTimeout(function() {
152
+ // Add custom styling or behavior after UI loads
153
+ const infoSection = document.querySelector('.swagger-ui .info');
154
+ if (infoSection && !infoSection.querySelector('.rapitapir-badge')) {
155
+ const badge = document.createElement('div');
156
+ badge.className = 'rapitapir-badge';
157
+ badge.innerHTML = '<small style="color: #666; font-style: italic;">Generated with RapiTapir Sinatra Extension</small>';
158
+ infoSection.appendChild(badge);
159
+ }
160
+ }, 1000);
161
+ };
162
+ JS
163
+ end
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require_relative 'sinatra/extension'
5
+
6
+ module RapiTapir
7
+ # SinatraRapiTapir - A clean base class for RapiTapir APIs
8
+ #
9
+ # This class provides the most ergonomic way to create RapiTapir APIs:
10
+ #
11
+ # class MyAPI < RapiTapir::SinatraRapiTapir
12
+ # rapitapir do
13
+ # info(title: 'My API', version: '1.0.0')
14
+ # development_defaults!
15
+ # end
16
+ #
17
+ # endpoint(
18
+ # GET('/hello').ok(string_response).build
19
+ # ) { { message: 'Hello!' } }
20
+ # end
21
+ #
22
+ # Features automatically included:
23
+ # - Enhanced HTTP verb DSL (GET, POST, PUT, etc.)
24
+ # - RapiTapir extension with all features
25
+ # - Clean inheritance-based setup
26
+ class SinatraRapiTapir < ::Sinatra::Base
27
+ # Automatically register the RapiTapir extension
28
+ register RapiTapir::Sinatra::Extension
29
+
30
+ # Include a helpful message for developers
31
+ configure :development do
32
+ puts '🚀 Using RapiTapir::SinatraRapiTapir base class'
33
+ puts '✨ Enhanced HTTP verb DSL automatically available'
34
+ puts '🔧 Extension features: health checks, CORS, docs, and more'
35
+ end
36
+ end
37
+ end
38
+
39
+ # Also make it available at the top level for convenience
40
+ SinatraRapiTapir = RapiTapir::SinatraRapiTapir
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'base'
4
+
5
+ module RapiTapir
6
+ module Types
7
+ # Array type for validating array values with element type constraints
8
+ # Validates arrays with specific element types and size constraints
9
+ class Array < Base
10
+ attr_reader :item_type
11
+
12
+ def initialize(item_type, min_items: nil, max_items: nil, unique_items: false, **options)
13
+ @item_type = item_type
14
+ super(
15
+ min_items: min_items,
16
+ max_items: max_items,
17
+ unique_items: unique_items,
18
+ **options
19
+ )
20
+ end
21
+
22
+ protected
23
+
24
+ def validate_type(value)
25
+ return ["Expected array, got #{value.class}"] unless value.is_a?(::Array)
26
+
27
+ []
28
+ end
29
+
30
+ def validate_constraints(value)
31
+ errors = []
32
+
33
+ errors.concat(validate_length_constraints(value))
34
+ errors.concat(validate_uniqueness_constraint(value))
35
+ errors.concat(validate_item_types(value))
36
+
37
+ errors
38
+ end
39
+
40
+ def validate_length_constraints(value)
41
+ errors = []
42
+
43
+ errors.concat(validate_min_items_constraint(value))
44
+ errors.concat(validate_max_items_constraint(value))
45
+
46
+ errors
47
+ end
48
+
49
+ def validate_min_items_constraint(value)
50
+ return [] unless constraints[:min_items] && value.length < constraints[:min_items]
51
+
52
+ ["Array length #{value.length} is below minimum #{constraints[:min_items]}"]
53
+ end
54
+
55
+ def validate_max_items_constraint(value)
56
+ return [] unless constraints[:max_items] && value.length > constraints[:max_items]
57
+
58
+ ["Array length #{value.length} exceeds maximum #{constraints[:max_items]}"]
59
+ end
60
+
61
+ def validate_uniqueness_constraint(value)
62
+ return [] unless constraints[:unique_items] && value.uniq.length != value.length
63
+
64
+ ['Array contains duplicate items but must be unique']
65
+ end
66
+
67
+ def validate_item_types(value)
68
+ errors = []
69
+
70
+ value.each_with_index do |item, index|
71
+ item_result = item_type.validate(item)
72
+ next if item_result[:valid]
73
+
74
+ item_result[:errors].each do |error|
75
+ errors << "Item at index #{index}: #{error}"
76
+ end
77
+ end
78
+
79
+ errors
80
+ end
81
+
82
+ def coerce_value(value)
83
+ case value
84
+ when ::Array
85
+ coerce_array_value(value)
86
+ when ::String
87
+ coerce_string_value(value)
88
+ else
89
+ coerce_single_value(value)
90
+ end
91
+ rescue JSON::ParserError => e
92
+ raise CoercionError.new(value, 'Array', "Invalid JSON: #{e.message}")
93
+ rescue StandardError => e
94
+ raise CoercionError.new(value, 'Array', e.message)
95
+ end
96
+
97
+ def coerce_array_value(value)
98
+ value.map { |item| item_type.coerce(item) }
99
+ end
100
+
101
+ def coerce_string_value(value)
102
+ # Try to parse as JSON array
103
+ require 'json'
104
+ parsed = JSON.parse(value)
105
+ raise CoercionError.new(value, 'Array', 'JSON string did not parse to array') unless parsed.is_a?(::Array)
106
+
107
+ parsed.map { |item| item_type.coerce(item) }
108
+ end
109
+
110
+ def coerce_single_value(value)
111
+ # Wrap single value in array
112
+ [item_type.coerce(value)]
113
+ end
114
+
115
+ def json_type
116
+ 'array'
117
+ end
118
+
119
+ def apply_constraints_to_schema(schema)
120
+ super
121
+ apply_array_specific_constraints(schema)
122
+ end
123
+
124
+ def apply_array_specific_constraints(schema)
125
+ schema[:items] = item_type.to_json_schema
126
+ apply_size_constraints(schema)
127
+ apply_uniqueness_constraint(schema)
128
+ end
129
+
130
+ def apply_size_constraints(schema)
131
+ schema[:minItems] = constraints[:min_items] if constraints[:min_items]
132
+ schema[:maxItems] = constraints[:max_items] if constraints[:max_items]
133
+ end
134
+
135
+ def apply_uniqueness_constraint(schema)
136
+ schema[:uniqueItems] = constraints[:unique_items] if constraints[:unique_items]
137
+ end
138
+
139
+ def to_s
140
+ item_type_str = format_item_type_string
141
+ constraint_part = format_constraints_string
142
+ "Array[#{item_type_str}]#{constraint_part}"
143
+ end
144
+
145
+ def format_item_type_string
146
+ item_type.respond_to?(:to_s) ? item_type.to_s : item_type.class.name
147
+ end
148
+
149
+ def format_constraints_string
150
+ constraint_strs = build_constraint_strings
151
+ constraint_strs.empty? ? '' : "(#{constraint_strs.join(', ')})"
152
+ end
153
+
154
+ def build_constraint_strings
155
+ constraint_strs = []
156
+ constraint_strs << "min_items: #{constraints[:min_items]}" if constraints[:min_items]
157
+ constraint_strs << "max_items: #{constraints[:max_items]}" if constraints[:max_items]
158
+ constraint_strs << "unique: #{constraints[:unique_items]}" if constraints[:unique_items]
159
+ constraint_strs
160
+ end
161
+ end
162
+ end
163
+ end