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,403 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RapiTapir
6
+ module OpenAPI
7
+ # OpenAPI schema generator for RapiTapir endpoints
8
+ # Converts RapiTapir endpoint definitions to OpenAPI 3.0 specifications
9
+ class SchemaGenerator
10
+ attr_reader :endpoints, :info, :servers
11
+
12
+ def initialize(endpoints: [], info: {}, servers: [])
13
+ @endpoints = endpoints
14
+ @info = default_info.merge(info)
15
+ @servers = servers.empty? ? default_servers : servers
16
+ end
17
+
18
+ # Generate complete OpenAPI 3.0 specification
19
+ def generate
20
+ {
21
+ openapi: '3.0.3',
22
+ info: @info,
23
+ servers: @servers,
24
+ paths: generate_paths,
25
+ components: generate_components
26
+ }
27
+ end
28
+
29
+ # Generate as JSON string
30
+ def to_json(pretty: true)
31
+ schema = generate
32
+ if pretty
33
+ JSON.pretty_generate(schema)
34
+ else
35
+ JSON.generate(schema)
36
+ end
37
+ end
38
+
39
+ # Generate as YAML string
40
+ def to_yaml
41
+ require 'yaml'
42
+ YAML.dump(generate)
43
+ end
44
+
45
+ private
46
+
47
+ def default_info
48
+ {
49
+ title: 'RapiTapir API',
50
+ version: '1.0.0',
51
+ description: 'API generated by RapiTapir'
52
+ }
53
+ end
54
+
55
+ def default_servers
56
+ [
57
+ {
58
+ url: 'http://localhost:4567',
59
+ description: 'Development server'
60
+ }
61
+ ]
62
+ end
63
+
64
+ def generate_paths
65
+ paths = {}
66
+
67
+ @endpoints.each do |endpoint_data|
68
+ endpoint = endpoint_data.is_a?(Hash) ? endpoint_data[:endpoint] : endpoint_data
69
+ # Convert :id format to {id} format for OpenAPI
70
+ path = convert_path_to_openapi(endpoint.path)
71
+ method = endpoint.method.to_s.downcase
72
+
73
+ paths[path] ||= {}
74
+ paths[path][method] = generate_operation(endpoint)
75
+ end
76
+
77
+ paths
78
+ end
79
+
80
+ def generate_operation(endpoint)
81
+ operation = build_base_operation(endpoint)
82
+
83
+ add_parameters_to_operation(operation, endpoint)
84
+ add_request_body_to_operation(operation, endpoint)
85
+ add_responses_to_operation(operation, endpoint)
86
+
87
+ # Remove nil values
88
+ operation.compact
89
+ end
90
+
91
+ def build_base_operation(endpoint)
92
+ {
93
+ summary: endpoint.metadata[:summary] || generate_default_summary(endpoint),
94
+ description: endpoint.metadata[:description],
95
+ operationId: generate_operation_id(endpoint),
96
+ tags: endpoint.metadata[:tags]&.any? ? endpoint.metadata[:tags] : [generate_default_tag(endpoint)]
97
+ }
98
+ end
99
+
100
+ def add_parameters_to_operation(operation, endpoint)
101
+ parameters = generate_parameters(endpoint)
102
+ operation[:parameters] = parameters if parameters.any?
103
+ end
104
+
105
+ def add_request_body_to_operation(operation, endpoint)
106
+ request_body = generate_request_body(endpoint)
107
+ operation[:requestBody] = request_body if request_body
108
+ end
109
+
110
+ def add_responses_to_operation(operation, endpoint)
111
+ operation[:responses] = generate_responses(endpoint)
112
+ end
113
+
114
+ def generate_parameters(endpoint)
115
+ parameters = []
116
+
117
+ endpoint.inputs.each do |input|
118
+ parameter = build_parameter_for_input(input)
119
+ parameters << parameter if parameter
120
+ end
121
+
122
+ parameters
123
+ end
124
+
125
+ def build_parameter_for_input(input)
126
+ case input.kind
127
+ when :path
128
+ build_path_parameter(input)
129
+ when :query
130
+ build_query_parameter(input)
131
+ when :header
132
+ build_header_parameter(input)
133
+ end
134
+ end
135
+
136
+ def build_path_parameter(input)
137
+ {
138
+ name: input.name.to_s,
139
+ in: 'path',
140
+ required: true,
141
+ description: "Path parameter: #{input.name}",
142
+ schema: type_to_schema(input.type)
143
+ }
144
+ end
145
+
146
+ def build_query_parameter(input)
147
+ {
148
+ name: input.name.to_s,
149
+ in: 'query',
150
+ required: input.required?,
151
+ description: "Query parameter: #{input.name}",
152
+ schema: type_to_schema(input.type)
153
+ }
154
+ end
155
+
156
+ def build_header_parameter(input)
157
+ {
158
+ name: input.name.to_s,
159
+ in: 'header',
160
+ required: input.required?,
161
+ description: "Header parameter: #{input.name}",
162
+ schema: type_to_schema(input.type)
163
+ }
164
+ end
165
+
166
+ def generate_request_body(endpoint)
167
+ body_input = endpoint.inputs.find { |input| input.kind == :body }
168
+ return nil unless body_input
169
+
170
+ content_type = determine_content_type(body_input)
171
+
172
+ {
173
+ description: 'Request body',
174
+ required: body_input.required?,
175
+ content: {
176
+ content_type => {
177
+ schema: type_to_schema(body_input.type)
178
+ }
179
+ }
180
+ }
181
+ end
182
+
183
+ def generate_responses(endpoint)
184
+ responses = {}
185
+
186
+ generate_success_responses(endpoint, responses)
187
+ generate_error_responses(endpoint, responses)
188
+ add_default_response_if_needed(responses)
189
+
190
+ responses
191
+ end
192
+
193
+ def generate_success_responses(endpoint, responses)
194
+ endpoint.outputs.each do |output|
195
+ status_code = determine_status_code(output)
196
+ content_type = determine_output_content_type(output)
197
+
198
+ responses[status_code.to_s] = {
199
+ description: 'Successful response',
200
+ content: {
201
+ content_type => {
202
+ schema: type_to_schema(output.type)
203
+ }
204
+ }
205
+ }
206
+ end
207
+ end
208
+
209
+ def generate_error_responses(endpoint, responses)
210
+ endpoint.errors.each do |error_entry|
211
+ error_response = build_error_response(error_entry)
212
+ responses[error_response[:status_code].to_s] = error_response[:response]
213
+ end
214
+ end
215
+
216
+ def build_error_response(error_entry)
217
+ if error_entry.respond_to?(:status_code)
218
+ build_enhanced_error_response(error_entry)
219
+ else
220
+ build_legacy_error_response(error_entry)
221
+ end
222
+ end
223
+
224
+ def build_enhanced_error_response(error_entry)
225
+ status_code = error_entry.status_code
226
+ description = error_entry.description || 'Error response'
227
+ schema = type_to_schema(error_entry.type || { error: :string })
228
+
229
+ {
230
+ status_code: status_code,
231
+ response: {
232
+ description: description,
233
+ content: {
234
+ 'application/json' => {
235
+ schema: schema
236
+ }
237
+ }
238
+ }
239
+ }
240
+ end
241
+
242
+ def build_legacy_error_response(error_entry)
243
+ status_code = error_entry[:code] || 400
244
+ description = error_entry[:description] || 'Error response'
245
+ schema = type_to_schema(error_entry[:output]&.type || { error: :string })
246
+
247
+ {
248
+ status_code: status_code,
249
+ response: {
250
+ description: description,
251
+ content: {
252
+ 'application/json' => {
253
+ schema: schema
254
+ }
255
+ }
256
+ }
257
+ }
258
+ end
259
+
260
+ def add_default_response_if_needed(responses)
261
+ return unless responses.empty?
262
+
263
+ responses['200'] = {
264
+ description: 'Successful response'
265
+ }
266
+ end
267
+
268
+ def generate_components
269
+ {
270
+ schemas: generate_schema_components
271
+ }
272
+ end
273
+
274
+ def generate_schema_components
275
+ # For now, return empty. In the future, we can extract common schemas
276
+ {}
277
+ end
278
+
279
+ def type_to_schema(type)
280
+ case type
281
+ when RapiTapir::Types::String, :string, String
282
+ { type: 'string' }
283
+ when RapiTapir::Types::Integer, :integer, Integer
284
+ { type: 'integer' }
285
+ when RapiTapir::Types::Float, :number, Float
286
+ { type: 'number' }
287
+ when RapiTapir::Types::Boolean, :boolean
288
+ { type: 'boolean' }
289
+ else
290
+ generate_complex_type_schema(type)
291
+ end
292
+ end
293
+
294
+ def generate_complex_type_schema(type)
295
+ case type
296
+ when RapiTapir::Types::Array
297
+ generate_array_schema(type)
298
+ when RapiTapir::Types::Hash
299
+ generate_hash_schema(type)
300
+ when :date, :datetime
301
+ generate_date_schema(type)
302
+ when Array
303
+ generate_ruby_array_schema(type)
304
+ else # Default for unknown types, :string, String, and Hash
305
+ { type: 'string' }
306
+ end
307
+ end
308
+
309
+ def generate_array_schema(type)
310
+ { type: 'array', items: type_to_schema(type.item_type) }
311
+ end
312
+
313
+ def generate_hash_schema(type)
314
+ return { type: 'object' } if type.field_types.empty?
315
+
316
+ properties = {}
317
+ required = []
318
+
319
+ type.field_types.each do |key, value|
320
+ properties[key.to_s] = type_to_schema(value)
321
+ required << key.to_s unless value.nil?
322
+ end
323
+
324
+ schema = { type: 'object', properties: properties }
325
+ schema[:required] = required if required.any?
326
+ schema
327
+ end
328
+
329
+ def generate_date_schema(type)
330
+ format = type == :date ? 'date' : 'date-time'
331
+ { type: 'string', format: format }
332
+ end
333
+
334
+ def generate_ruby_array_schema(type)
335
+ if type.length == 1
336
+ { type: 'array', items: type_to_schema(type.first) }
337
+ else
338
+ { type: 'array', items: { type: 'string' } }
339
+ end
340
+ end
341
+
342
+ def determine_content_type(input)
343
+ case input.type
344
+ when Hash
345
+ # For hash types, default to JSON
346
+ return 'application/json'
347
+ end
348
+ 'application/json'
349
+ end
350
+
351
+ def determine_output_content_type(output)
352
+ case output.kind
353
+ when :xml
354
+ 'application/xml'
355
+ when :html
356
+ 'text/html'
357
+ else # Default for :json and unknown formats
358
+ 'application/json'
359
+ end
360
+ end
361
+
362
+ def determine_status_code(output)
363
+ if output.kind == :status && output.type.is_a?(Integer)
364
+ output.type
365
+ else
366
+ # Default status codes based on output type
367
+ case output.kind
368
+ when :json, :xml
369
+ # JSON and XML responses default to 200
370
+ return 200
371
+ end
372
+ 200
373
+ end
374
+ end
375
+
376
+ def generate_operation_id(endpoint)
377
+ method = endpoint.method.to_s.downcase
378
+ path_parts = endpoint.path.split('/').reject(&:empty?).map do |part|
379
+ part.start_with?(':') ? part[1..] : part
380
+ end
381
+
382
+ "#{method}_#{path_parts.join('_')}"
383
+ end
384
+
385
+ def generate_default_summary(endpoint)
386
+ method = endpoint.method.to_s.upcase
387
+ path = endpoint.path
388
+ "#{method} #{path}"
389
+ end
390
+
391
+ def generate_default_tag(endpoint)
392
+ # Extract the first path segment as a tag
393
+ first_segment = endpoint.path.split('/').reject(&:empty?).first
394
+ first_segment&.capitalize || 'default'
395
+ end
396
+
397
+ def convert_path_to_openapi(path)
398
+ # Convert "/users/:id" to "/users/{id}" for OpenAPI
399
+ path.gsub(/:([a-zA-Z_]\w*)/, '{\1}')
400
+ end
401
+ end
402
+ end
403
+ end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'types'
4
+
5
+ module RapiTapir
6
+ # Schema definition and validation module
7
+ # Provides tools for defining and validating data schemas
8
+ module Schema
9
+ # Error for schema validation failures
10
+ # Raised when data does not conform to defined schema constraints
11
+ class ValidationError < StandardError
12
+ attr_reader :errors
13
+
14
+ def initialize(errors)
15
+ @errors = errors
16
+ super(build_message)
17
+ end
18
+
19
+ private
20
+
21
+ def build_message
22
+ "Schema validation failed:\n#{errors.map { |error| " - #{error}" }.join("\n")}"
23
+ end
24
+ end
25
+
26
+ # Define a schema using a block
27
+ def self.define(&block)
28
+ builder = SchemaBuilder.new
29
+ builder.instance_eval(&block)
30
+ builder.build
31
+ end
32
+
33
+ # Validate a value against a type
34
+ def self.validate!(value, type)
35
+ result = type.validate(value)
36
+ return value if result[:valid]
37
+
38
+ raise ValidationError, result[:errors]
39
+ end
40
+
41
+ # Validate a value against a type, returning result
42
+ def self.validate(value, type)
43
+ type.validate(value)
44
+ end
45
+
46
+ # Coerce a value using a type
47
+ def self.coerce(value, type)
48
+ type.coerce(value)
49
+ end
50
+
51
+ # Create a type from a simplified definition
52
+ def self.from_definition(definition)
53
+ case definition
54
+ when Symbol
55
+ create_primitive_type(definition)
56
+ when ::Hash
57
+ create_type_from_hash(definition)
58
+ when ::Array
59
+ create_array_type_from_definition(definition)
60
+ when Class
61
+ # Assume it's already a type class
62
+ definition
63
+ else
64
+ raise ArgumentError, "Unknown definition type: #{definition.class}"
65
+ end
66
+ end
67
+
68
+ def self.create_type_from_hash(definition)
69
+ if definition.keys == [:type] && definition[:type].is_a?(Symbol)
70
+ create_primitive_type(definition[:type])
71
+ else
72
+ create_object_from_hash(definition)
73
+ end
74
+ end
75
+
76
+ def self.create_array_type_from_definition(definition)
77
+ raise ArgumentError, 'Array definition must have exactly one element type' unless definition.length == 1
78
+
79
+ Types.array(from_definition(definition.first))
80
+ end
81
+
82
+ PRIMITIVE_TYPE_MAP = {
83
+ string: -> { Types.string },
84
+ integer: -> { Types.integer },
85
+ float: -> { Types.float },
86
+ boolean: -> { Types.boolean },
87
+ date: -> { Types.date },
88
+ datetime: -> { Types.datetime },
89
+ uuid: -> { Types.uuid },
90
+ email: -> { Types.email }
91
+ }.freeze
92
+
93
+ def self.create_primitive_type(type_symbol)
94
+ type_creator = PRIMITIVE_TYPE_MAP[type_symbol]
95
+ return type_creator.call if type_creator
96
+
97
+ raise ArgumentError, "Unknown primitive type: #{type_symbol}"
98
+ end
99
+
100
+ def self.create_object_from_hash(hash_definition)
101
+ object_type = Types.object
102
+
103
+ hash_definition.each do |field_name, field_definition|
104
+ field_type = from_definition(field_definition)
105
+ object_type.field(field_name, field_type)
106
+ end
107
+
108
+ object_type
109
+ end
110
+
111
+ # Builder for constructing complex schemas from definitions
112
+ # Provides a fluent interface for schema creation
113
+ class SchemaBuilder
114
+ def initialize
115
+ @object_type = Types.object
116
+ end
117
+
118
+ def field(name, type_def, required: true, **options)
119
+ type = Schema.from_definition(type_def)
120
+ @object_type.field(name, type, required: required, **options)
121
+ end
122
+
123
+ def required_field(name, type_def, **options)
124
+ field(name, type_def, required: true, **options)
125
+ end
126
+
127
+ def optional_field(name, type_def, **options)
128
+ field(name, type_def, required: false, **options)
129
+ end
130
+
131
+ def build
132
+ @object_type
133
+ end
134
+ end
135
+ end
136
+ end