easy_talk 3.1.0 → 3.2.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 (53) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +75 -0
  5. data/README.md +616 -35
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +55 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/property-types.markdown +212 -0
  14. data/docs/schema-definition.markdown +180 -0
  15. data/lib/easy_talk/builders/base_builder.rb +4 -2
  16. data/lib/easy_talk/builders/composition_builder.rb +10 -12
  17. data/lib/easy_talk/builders/object_builder.rb +45 -30
  18. data/lib/easy_talk/builders/registry.rb +168 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +15 -4
  20. data/lib/easy_talk/configuration.rb +31 -1
  21. data/lib/easy_talk/error_formatter/base.rb +100 -0
  22. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  23. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  24. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  25. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  26. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  27. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  28. data/lib/easy_talk/error_formatter.rb +143 -0
  29. data/lib/easy_talk/errors.rb +2 -0
  30. data/lib/easy_talk/errors_helper.rb +63 -34
  31. data/lib/easy_talk/model.rb +123 -90
  32. data/lib/easy_talk/model_helper.rb +13 -0
  33. data/lib/easy_talk/naming_strategies.rb +20 -0
  34. data/lib/easy_talk/property.rb +16 -94
  35. data/lib/easy_talk/ref_helper.rb +27 -0
  36. data/lib/easy_talk/schema.rb +198 -0
  37. data/lib/easy_talk/schema_definition.rb +7 -1
  38. data/lib/easy_talk/schema_methods.rb +80 -0
  39. data/lib/easy_talk/tools/function_builder.rb +1 -1
  40. data/lib/easy_talk/type_introspection.rb +178 -0
  41. data/lib/easy_talk/types/base_composer.rb +2 -1
  42. data/lib/easy_talk/types/composer.rb +4 -0
  43. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
  44. data/lib/easy_talk/validation_adapters/base.rb +144 -0
  45. data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
  46. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  47. data/lib/easy_talk/validation_builder.rb +28 -309
  48. data/lib/easy_talk/version.rb +1 -1
  49. data/lib/easy_talk.rb +41 -0
  50. metadata +26 -4
  51. data/docs/404.html +0 -25
  52. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  53. data/easy_talk.gemspec +0 -39
@@ -45,28 +45,6 @@ module EasyTalk
45
45
  # @return [Hash<Symbol, Object>] Additional constraints applied to the property
46
46
  attr_reader :constraints
47
47
 
48
- # Mapping of Ruby type names to their corresponding schema builder classes.
49
- # Each builder knows how to convert a specific Ruby type to JSON Schema.
50
- #
51
- # @api private
52
- TYPE_TO_BUILDER = {
53
- 'String' => Builders::StringBuilder,
54
- 'Integer' => Builders::IntegerBuilder,
55
- 'Float' => Builders::NumberBuilder,
56
- 'BigDecimal' => Builders::NumberBuilder,
57
- 'T::Boolean' => Builders::BooleanBuilder,
58
- 'TrueClass' => Builders::BooleanBuilder,
59
- 'NilClass' => Builders::NullBuilder,
60
- 'Date' => Builders::TemporalBuilder::DateBuilder,
61
- 'DateTime' => Builders::TemporalBuilder::DatetimeBuilder,
62
- 'Time' => Builders::TemporalBuilder::TimeBuilder,
63
- 'anyOf' => Builders::CompositionBuilder::AnyOfBuilder,
64
- 'allOf' => Builders::CompositionBuilder::AllOfBuilder,
65
- 'oneOf' => Builders::CompositionBuilder::OneOfBuilder,
66
- 'T::Types::TypedArray' => Builders::TypedArrayBuilder,
67
- 'T::Types::Union' => Builders::UnionBuilder
68
- }.freeze
69
-
70
48
  # Initializes a new instance of the Property class.
71
49
  #
72
50
  # @param name [Symbol] The name of the property
@@ -124,17 +102,20 @@ module EasyTalk
124
102
  def build
125
103
  if nilable_type?
126
104
  build_nilable_schema
127
- elsif should_use_ref?
128
- build_ref_schema
129
- elsif builder
130
- args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
131
- builder.new(*args).build
105
+ elsif RefHelper.should_use_ref?(type, constraints)
106
+ RefHelper.build_ref_schema(type, constraints)
107
+ elsif (resolved = find_builder_for_type)
108
+ builder_class, is_collection = resolved
109
+ args = is_collection ? [name, type, constraints] : [name, constraints]
110
+ builder_class.new(*args).build
132
111
  elsif type.respond_to?(:schema)
133
112
  # merge the top-level constraints from *this* property
134
113
  # e.g. :title, :description, :default, etc
135
114
  type.schema.merge!(constraints)
136
115
  else
137
- 'object'
116
+ raise UnknownTypeError,
117
+ "Unknown type '#{type.inspect}' for property '#{name}'. " \
118
+ 'Register a custom builder with EasyTalk.register_type or use a supported type.'
138
119
  end
139
120
  end
140
121
 
@@ -151,28 +132,18 @@ module EasyTalk
151
132
  build.as_json
152
133
  end
153
134
 
154
- # Returns the builder class associated with the property type.
155
- #
156
- # The builder is responsible for converting the Ruby type to a JSON Schema type.
157
- #
158
- # @return [Class, nil] The builder class for this property's type, or nil if no dedicated builder exists
159
- # @see #find_builder_for_type
160
- def builder
161
- @builder ||= find_builder_for_type
162
- end
163
-
164
135
  private
165
136
 
166
- # Finds the appropriate builder for the current type.
137
+ # Finds the appropriate builder for the current type using the Builders::Registry.
167
138
  #
168
- # First checks if there's a builder for the class name, then falls back
169
- # to checking if there's a builder for the type's name (if it responds to :name).
139
+ # The registry is checked for a matching builder based on the type's class name
140
+ # or the type's own name.
170
141
  #
171
- # @return [Class, nil] The builder class for this type, or nil if none matches
142
+ # @return [Array(Class, Boolean), nil] A tuple of [builder_class, is_collection] or nil if none matches
172
143
  # @api private
144
+ # @see Builders::Registry.resolve
173
145
  def find_builder_for_type
174
- TYPE_TO_BUILDER[type.class.name.to_s] ||
175
- (type.respond_to?(:name) ? TYPE_TO_BUILDER[type.name.to_s] : nil)
146
+ Builders::Registry.resolve(type)
176
147
  end
177
148
 
178
149
  # Determines if the type is nilable (can be nil).
@@ -203,7 +174,7 @@ module EasyTalk
203
174
  return { type: 'null' } unless actual_type
204
175
 
205
176
  # Check if the underlying type is an EasyTalk model that should use $ref
206
- if easytalk_model?(actual_type) && should_use_ref_for_type?(actual_type)
177
+ if RefHelper.should_use_ref_for_type?(actual_type, constraints)
207
178
  # Use anyOf with $ref and null type
208
179
  ref_constraints = constraints.except(:ref, :optional)
209
180
  schema = { anyOf: [{ '$ref': actual_type.ref_template }, { type: 'null' }] }
@@ -218,54 +189,5 @@ module EasyTalk
218
189
  type: [non_nil_schema[:type], 'null'].compact
219
190
  )
220
191
  end
221
-
222
- # Determines if $ref should be used for the current type.
223
- #
224
- # @return [Boolean] true if $ref should be used, false otherwise
225
- # @api private
226
- def should_use_ref?
227
- return false unless easytalk_model?(type)
228
-
229
- should_use_ref_for_type?(type)
230
- end
231
-
232
- # Determines if $ref should be used for a given type based on constraints and config.
233
- #
234
- # @param check_type [Class] The type to check
235
- # @return [Boolean] true if $ref should be used, false otherwise
236
- # @api private
237
- def should_use_ref_for_type?(check_type)
238
- return false unless easytalk_model?(check_type)
239
-
240
- # Per-property constraint takes precedence
241
- return constraints[:ref] if constraints.key?(:ref)
242
-
243
- # Fall back to global configuration
244
- EasyTalk.configuration.use_refs
245
- end
246
-
247
- # Checks if a type is an EasyTalk model.
248
- #
249
- # @param check_type [Object] The type to check
250
- # @return [Boolean] true if the type is an EasyTalk model
251
- # @api private
252
- def easytalk_model?(check_type)
253
- check_type.is_a?(Class) &&
254
- check_type.respond_to?(:schema) &&
255
- check_type.respond_to?(:ref_template) &&
256
- defined?(EasyTalk::Model) &&
257
- check_type.include?(EasyTalk::Model)
258
- end
259
-
260
- # Builds a $ref schema for an EasyTalk model.
261
- #
262
- # @return [Hash] A schema with $ref pointing to the model's definition
263
- # @api private
264
- def build_ref_schema
265
- # Remove ref and optional from constraints as they're not JSON Schema keywords
266
- ref_constraints = constraints.except(:ref, :optional)
267
- schema = { '$ref': type.ref_template }
268
- ref_constraints.empty? ? schema : schema.merge(ref_constraints)
269
- end
270
192
  end
271
193
  end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'model_helper'
4
+
5
+ module EasyTalk
6
+ module RefHelper
7
+ def self.should_use_ref?(type, constraints)
8
+ ModelHelper.easytalk_model?(type) && should_use_ref_for_type?(type, constraints)
9
+ end
10
+
11
+ def self.should_use_ref_for_type?(type, constraints)
12
+ return false unless ModelHelper.easytalk_model?(type)
13
+
14
+ # Per-property constraint takes precedence
15
+ if constraints.key?(:ref)
16
+ constraints[:ref]
17
+ else
18
+ EasyTalk.configuration.use_refs
19
+ end
20
+ end
21
+
22
+ def self.build_ref_schema(type, constraints)
23
+ # Remove ref and optional from constraints as they're not JSON Schema keywords
24
+ { '$ref': type.ref_template }.merge(constraints.except(:ref, :optional))
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'active_support'
5
+ require 'active_support/core_ext'
6
+ require 'active_support/time'
7
+ require 'active_support/concern'
8
+ require 'active_support/json'
9
+ require_relative 'builders/object_builder'
10
+ require_relative 'schema_definition'
11
+
12
+ module EasyTalk
13
+ # A lightweight module for schema generation without ActiveModel validations.
14
+ #
15
+ # Use this module when you need JSON Schema generation without the overhead
16
+ # of ActiveModel validations. This is ideal for:
17
+ # - API documentation and OpenAPI spec generation
18
+ # - Schema-first design where validation happens elsewhere
19
+ # - High-performance scenarios where validation overhead is unwanted
20
+ # - Generating schemas for external systems
21
+ #
22
+ # Unlike EasyTalk::Model, this module does NOT include ActiveModel::API or
23
+ # ActiveModel::Validations, so instances will not respond to `valid?` or have
24
+ # validation errors.
25
+ #
26
+ # @example Basic usage
27
+ # class ApiContract
28
+ # include EasyTalk::Schema
29
+ #
30
+ # define_schema do
31
+ # title 'API Contract'
32
+ # property :name, String, min_length: 2
33
+ # property :age, Integer, minimum: 0
34
+ # end
35
+ # end
36
+ #
37
+ # ApiContract.json_schema # => { "type" => "object", ... }
38
+ # contract = ApiContract.new(name: 'Test', age: 25)
39
+ # contract.name # => 'Test'
40
+ # contract.valid? # => NoMethodError (no ActiveModel)
41
+ #
42
+ # @see EasyTalk::Model For a full-featured module with validations
43
+ #
44
+ module Schema
45
+ def self.included(base)
46
+ base.extend(ClassMethods)
47
+ base.include(InstanceMethods)
48
+ end
49
+
50
+ # Instance methods for schema-only models.
51
+ module InstanceMethods
52
+ # Initialize the schema object with attributes.
53
+ #
54
+ # @param attributes [Hash] The attributes to set
55
+ def initialize(attributes = {})
56
+ @additional_properties = {}
57
+ schema_def = self.class.schema_definition
58
+
59
+ return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
60
+
61
+ (schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
62
+ value = attributes[prop_name] || attributes[prop_name.to_s]
63
+
64
+ # Handle default values
65
+ if value.nil? && !attributes.key?(prop_name) && !attributes.key?(prop_name.to_s)
66
+ default_value = prop_definition.dig(:constraints, :default)
67
+ value = default_value unless default_value.nil?
68
+ end
69
+
70
+ # Handle nested EasyTalk::Schema or EasyTalk::Model objects
71
+ defined_type = prop_definition[:type]
72
+ nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
73
+ defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
74
+
75
+ if defined_type.is_a?(Class) &&
76
+ (defined_type.include?(EasyTalk::Schema) || defined_type.include?(EasyTalk::Model)) &&
77
+ value.is_a?(Hash)
78
+ value = defined_type.new(value)
79
+ end
80
+
81
+ instance_variable_set("@#{prop_name}", value)
82
+ end
83
+ end
84
+
85
+ # Convert defined properties to a hash.
86
+ #
87
+ # @return [Hash] The properties as a hash
88
+ def to_hash
89
+ properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
90
+ return {} if properties_to_include.empty?
91
+
92
+ properties_to_include.each_with_object({}) do |prop, hash|
93
+ hash[prop.to_s] = send(prop)
94
+ end
95
+ end
96
+
97
+ # Convert to JSON-compatible hash including additional properties.
98
+ #
99
+ # @param _options [Hash] JSON options (ignored)
100
+ # @return [Hash] The combined hash
101
+ def as_json(_options = {})
102
+ to_hash.merge(@additional_properties)
103
+ end
104
+
105
+ # Convert to hash including additional properties.
106
+ #
107
+ # @return [Hash] The combined hash
108
+ def to_h
109
+ to_hash.merge(@additional_properties)
110
+ end
111
+
112
+ # Allow comparison with hashes.
113
+ #
114
+ # @param other [Object] The object to compare with
115
+ # @return [Boolean] True if equal
116
+ def ==(other)
117
+ case other
118
+ when Hash
119
+ self_hash = (self.class.schema_definition.schema[:properties] || {}).keys.each_with_object({}) do |prop, hash|
120
+ hash[prop] = send(prop)
121
+ end
122
+ other_normalized = other.transform_keys(&:to_sym)
123
+ self_hash == other_normalized
124
+ else
125
+ super
126
+ end
127
+ end
128
+ end
129
+
130
+ # Class methods for schema-only models.
131
+ module ClassMethods
132
+ include SchemaMethods
133
+
134
+ # Returns the schema for the model.
135
+ #
136
+ # @return [Hash] The schema for the model.
137
+ def schema
138
+ @schema ||= if defined?(@schema_definition) && @schema_definition
139
+ build_schema(@schema_definition)
140
+ else
141
+ {}
142
+ end
143
+ end
144
+
145
+ # Define the schema for the model using the provided block.
146
+ # Unlike EasyTalk::Model, this does NOT apply any validations.
147
+ #
148
+ # @yield The block to define the schema.
149
+ # @raise [ArgumentError] If the class does not have a name.
150
+ def define_schema(&)
151
+ raise ArgumentError, 'The class must have a name' unless name.present?
152
+
153
+ @schema_definition = SchemaDefinition.new(name)
154
+ @schema_definition.klass = self
155
+ @schema_definition.instance_eval(&)
156
+
157
+ # Define accessors for all properties
158
+ defined_properties = (@schema_definition.schema[:properties] || {}).keys
159
+ attr_accessor(*defined_properties)
160
+
161
+ # NO validations are applied - this is schema-only
162
+
163
+ @schema_definition
164
+ end
165
+
166
+ # Returns the schema definition for the model.
167
+ #
168
+ # @return [SchemaDefinition] The schema definition.
169
+ def schema_definition
170
+ @schema_definition ||= {}
171
+ end
172
+
173
+ # Check if additional properties are allowed.
174
+ #
175
+ # @return [Boolean] True if additional properties are allowed.
176
+ def additional_properties_allowed?
177
+ @schema_definition&.schema&.fetch(:additional_properties, false)
178
+ end
179
+
180
+ # Returns the property names defined in the schema.
181
+ #
182
+ # @return [Array<Symbol>] Array of property names as symbols.
183
+ def properties
184
+ (@schema_definition&.schema&.dig(:properties) || {}).keys
185
+ end
186
+
187
+ private
188
+
189
+ # Builds the schema using the provided schema definition.
190
+ #
191
+ # @param schema_definition [SchemaDefinition] The schema definition.
192
+ # @return [Hash] The built schema.
193
+ def build_schema(schema_definition)
194
+ Builders::ObjectBuilder.new(schema_definition).build
195
+ end
196
+ end
197
+ end
198
+ end
@@ -24,6 +24,7 @@ module EasyTalk
24
24
  @schema[:additional_properties] = false unless schema.key?(:additional_properties)
25
25
  @name = name
26
26
  @klass = nil # Initialize klass to nil
27
+ @property_naming_strategy = EasyTalk.configuration.property_naming_strategy
27
28
  end
28
29
 
29
30
  EasyTalk::KEYWORDS.each do |keyword|
@@ -38,7 +39,8 @@ module EasyTalk
38
39
  end
39
40
 
40
41
  def property(name, type, constraints = {}, &)
41
- validate_property_name(name)
42
+ constraints[:as] ||= @property_naming_strategy.call(name)
43
+ validate_property_name(constraints[:as])
42
44
  @schema[:properties] ||= {}
43
45
 
44
46
  if block_given?
@@ -76,5 +78,9 @@ module EasyTalk
76
78
  # Call standard property method
77
79
  property(name, nilable_type, constraints)
78
80
  end
81
+
82
+ def property_naming_strategy(strategy)
83
+ @property_naming_strategy = EasyTalk::NamingStrategies.derive_strategy(strategy)
84
+ end
79
85
  end
80
86
  end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ # Shared methods for JSON Schema generation.
5
+ #
6
+ # This module provides common functionality for building JSON schemas,
7
+ # including $schema and $id resolution. It is included in both
8
+ # EasyTalk::Model and EasyTalk::Schema to avoid code duplication.
9
+ #
10
+ # @note Classes including this module must define:
11
+ # - `name` - The class name (used in ref_template)
12
+ # - `schema` - The built schema hash (used in json_schema)
13
+ # - `@schema_definition` - Instance variable with schema metadata
14
+ #
15
+ module SchemaMethods
16
+ # Returns the reference template for the model.
17
+ #
18
+ # @return [String] The reference template for the model.
19
+ def ref_template
20
+ "#/$defs/#{name}"
21
+ end
22
+
23
+ # Returns the JSON schema for the model.
24
+ # This is the final output that includes the $schema keyword if configured.
25
+ #
26
+ # @return [Hash] The JSON schema for the model.
27
+ def json_schema
28
+ @json_schema ||= build_json_schema
29
+ end
30
+
31
+ private
32
+
33
+ # Builds the final JSON schema with optional $schema and $id keywords.
34
+ #
35
+ # @return [Hash] The JSON schema.
36
+ def build_json_schema
37
+ result = schema.as_json
38
+ schema_uri = resolve_schema_uri
39
+ id_uri = resolve_schema_id
40
+
41
+ prefix = {}
42
+ prefix['$schema'] = schema_uri if schema_uri
43
+ prefix['$id'] = id_uri if id_uri
44
+
45
+ return result if prefix.empty?
46
+
47
+ prefix.merge(result)
48
+ end
49
+
50
+ # Resolves the schema URI from per-model setting or global config.
51
+ #
52
+ # @return [String, nil] The schema URI.
53
+ def resolve_schema_uri
54
+ model_version = @schema_definition&.schema&.dig(:schema_version)
55
+
56
+ if model_version
57
+ return nil if model_version == :none
58
+
59
+ Configuration::SCHEMA_VERSIONS[model_version] || model_version.to_s
60
+ else
61
+ EasyTalk.configuration.schema_uri
62
+ end
63
+ end
64
+
65
+ # Resolves the schema ID from per-model setting or global config.
66
+ #
67
+ # @return [String, nil] The schema ID.
68
+ def resolve_schema_id
69
+ model_id = @schema_definition&.schema&.dig(:schema_id)
70
+
71
+ if model_id
72
+ return nil if model_id == :none
73
+
74
+ model_id.to_s
75
+ else
76
+ EasyTalk.configuration.schema_id
77
+ end
78
+ end
79
+ end
80
+ end
@@ -27,7 +27,7 @@ module EasyTalk
27
27
 
28
28
  def generate_function_description(model)
29
29
  if model.respond_to?(:instructions)
30
- raise Instructor::Error, 'The instructions must be a string' unless model.instructions.is_a?(String)
30
+ raise InvalidInstructionsError, 'The instructions must be a string' unless model.instructions.is_a?(String)
31
31
 
32
32
  model.instructions
33
33
  else
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bigdecimal'
4
+
5
+ module EasyTalk
6
+ # Centralized module for robust type introspection.
7
+ #
8
+ # This module provides predicate methods for detecting types without relying
9
+ # on brittle string-based checks. It uses Sorbet's type system properly and
10
+ # handles edge cases gracefully.
11
+ #
12
+ # @example Checking if a type is boolean
13
+ # TypeIntrospection.boolean_type?(T::Boolean) # => true
14
+ # TypeIntrospection.boolean_type?(TrueClass) # => true
15
+ # TypeIntrospection.boolean_type?(String) # => false
16
+ #
17
+ # @example Getting JSON Schema type
18
+ # TypeIntrospection.json_schema_type(Integer) # => 'integer'
19
+ # TypeIntrospection.json_schema_type(Float) # => 'number'
20
+ #
21
+ module TypeIntrospection
22
+ # Mapping of Ruby classes to JSON Schema types
23
+ PRIMITIVE_TO_JSON_SCHEMA = {
24
+ String => 'string',
25
+ Integer => 'integer',
26
+ Float => 'number',
27
+ BigDecimal => 'number',
28
+ TrueClass => 'boolean',
29
+ FalseClass => 'boolean',
30
+ NilClass => 'null'
31
+ }.freeze
32
+
33
+ class << self
34
+ # Check if type represents a boolean (T::Boolean or TrueClass/FalseClass).
35
+ #
36
+ # @param type [Object] The type to check
37
+ # @return [Boolean] true if the type is a boolean type
38
+ #
39
+ # @example
40
+ # boolean_type?(T::Boolean) # => true
41
+ # boolean_type?(TrueClass) # => true
42
+ # boolean_type?(FalseClass) # => true
43
+ # boolean_type?(String) # => false
44
+ def boolean_type?(type)
45
+ return false if type.nil?
46
+ return true if [TrueClass, FalseClass].include?(type)
47
+ return true if type.respond_to?(:raw_type) && [TrueClass, FalseClass].include?(type.raw_type)
48
+
49
+ # Check for T::Boolean which is a TypeAlias with name 'T::Boolean'
50
+ return true if type.respond_to?(:name) && type.name == 'T::Boolean'
51
+
52
+ # Check for union types containing TrueClass and FalseClass
53
+ if type.respond_to?(:types)
54
+ type_classes = type.types.map { |t| t.respond_to?(:raw_type) ? t.raw_type : t }
55
+ return type_classes.sort_by(&:name) == [FalseClass, TrueClass].sort_by(&:name)
56
+ end
57
+
58
+ false
59
+ end
60
+
61
+ # Check if type is a typed array (T::Array[...]).
62
+ #
63
+ # @param type [Object] The type to check
64
+ # @return [Boolean] true if the type is a typed array
65
+ #
66
+ # @example
67
+ # typed_array?(T::Array[String]) # => true
68
+ # typed_array?(Array) # => false
69
+ def typed_array?(type)
70
+ return false if type.nil?
71
+
72
+ type.is_a?(T::Types::TypedArray)
73
+ end
74
+
75
+ # Check if type is nilable (T.nilable(...)).
76
+ #
77
+ # @param type [Object] The type to check
78
+ # @return [Boolean] true if the type is nilable
79
+ #
80
+ # @example
81
+ # nilable_type?(T.nilable(String)) # => true
82
+ # nilable_type?(String) # => false
83
+ def nilable_type?(type)
84
+ return false if type.nil?
85
+
86
+ type.respond_to?(:nilable?) && type.nilable?
87
+ end
88
+
89
+ # Check if type is a primitive Ruby type.
90
+ #
91
+ # @param type [Object] The type to check
92
+ # @return [Boolean] true if the type is a primitive
93
+ def primitive_type?(type)
94
+ return false if type.nil?
95
+
96
+ resolved = if type.is_a?(Class)
97
+ type
98
+ elsif type.respond_to?(:raw_type)
99
+ type.raw_type
100
+ end
101
+ PRIMITIVE_TO_JSON_SCHEMA.key?(resolved)
102
+ end
103
+
104
+ # Get JSON Schema type string for a Ruby type.
105
+ #
106
+ # @param type [Object] The type to convert
107
+ # @return [String] The JSON Schema type string
108
+ #
109
+ # @example
110
+ # json_schema_type(Integer) # => 'integer'
111
+ # json_schema_type(Float) # => 'number'
112
+ # json_schema_type(BigDecimal) # => 'number'
113
+ # json_schema_type(String) # => 'string'
114
+ def json_schema_type(type)
115
+ return 'object' if type.nil?
116
+ return 'boolean' if boolean_type?(type)
117
+
118
+ resolved_class = if type.is_a?(Class)
119
+ type
120
+ elsif type.respond_to?(:raw_type)
121
+ type.raw_type
122
+ end
123
+
124
+ PRIMITIVE_TO_JSON_SCHEMA[resolved_class] || resolved_class&.name&.downcase || 'object'
125
+ end
126
+
127
+ # Get the Ruby class for a type, handling Sorbet types.
128
+ #
129
+ # @param type [Object] The type to resolve
130
+ # @return [Class, Array<Class>, nil] The resolved class or classes
131
+ #
132
+ # @example
133
+ # get_type_class(String) # => String
134
+ # get_type_class(T::Boolean) # => [TrueClass, FalseClass]
135
+ # get_type_class(T::Array[String]) # => Array
136
+ def get_type_class(type)
137
+ return nil if type.nil?
138
+ return type if type.is_a?(Class)
139
+ return type.raw_type if type.respond_to?(:raw_type)
140
+ return Array if typed_array?(type)
141
+ return [TrueClass, FalseClass] if boolean_type?(type)
142
+
143
+ if nilable_type?(type)
144
+ inner = extract_inner_type(type)
145
+ return get_type_class(inner) if inner && inner != type
146
+ end
147
+
148
+ nil
149
+ end
150
+
151
+ # Extract inner type from nilable or complex types.
152
+ #
153
+ # @param type [Object] The type to unwrap
154
+ # @return [Object] The inner type, or the original type if not wrapped
155
+ #
156
+ # @example
157
+ # extract_inner_type(T.nilable(String)) # => String
158
+ def extract_inner_type(type)
159
+ return type if type.nil?
160
+
161
+ if type.respond_to?(:unwrap_nilable)
162
+ unwrapped = type.unwrap_nilable
163
+ return unwrapped.respond_to?(:raw_type) ? unwrapped.raw_type : unwrapped
164
+ end
165
+
166
+ if type.respond_to?(:types)
167
+ non_nil = type.types.find do |t|
168
+ raw = t.respond_to?(:raw_type) ? t.raw_type : t
169
+ raw != NilClass
170
+ end
171
+ return non_nil.respond_to?(:raw_type) ? non_nil.raw_type : non_nil if non_nil
172
+ end
173
+
174
+ type
175
+ end
176
+ end
177
+ end
178
+ end
@@ -3,7 +3,7 @@
3
3
  module EasyTalk
4
4
  module Types
5
5
  # no-doc
6
- class BaseComposer
6
+ class BaseComposer < T::Types::Base
7
7
  extend T::Sig
8
8
  extend T::Generic
9
9
 
@@ -16,6 +16,7 @@ module EasyTalk
16
16
  #
17
17
  # @param args [Array] the items to be assigned to the instance variable @items
18
18
  def initialize(*args)
19
+ super()
19
20
  @items = args
20
21
  end
21
22
  end