easy_talk 3.0.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +4 -0
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +105 -0
  5. data/README.md +1268 -40
  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 +119 -10
  18. data/lib/easy_talk/builders/registry.rb +168 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +20 -6
  20. data/lib/easy_talk/configuration.rb +51 -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/keywords.rb +2 -0
  32. data/lib/easy_talk/model.rb +125 -41
  33. data/lib/easy_talk/model_helper.rb +13 -0
  34. data/lib/easy_talk/naming_strategies.rb +20 -0
  35. data/lib/easy_talk/property.rb +32 -44
  36. data/lib/easy_talk/ref_helper.rb +27 -0
  37. data/lib/easy_talk/schema.rb +198 -0
  38. data/lib/easy_talk/schema_definition.rb +7 -1
  39. data/lib/easy_talk/schema_methods.rb +80 -0
  40. data/lib/easy_talk/tools/function_builder.rb +1 -1
  41. data/lib/easy_talk/type_introspection.rb +178 -0
  42. data/lib/easy_talk/types/base_composer.rb +2 -1
  43. data/lib/easy_talk/types/composer.rb +4 -0
  44. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +329 -0
  45. data/lib/easy_talk/validation_adapters/base.rb +144 -0
  46. data/lib/easy_talk/validation_adapters/none_adapter.rb +36 -0
  47. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  48. data/lib/easy_talk/validation_builder.rb +28 -309
  49. data/lib/easy_talk/version.rb +1 -1
  50. data/lib/easy_talk.rb +41 -0
  51. metadata +28 -6
  52. data/docs/404.html +0 -25
  53. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  54. 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
@@ -101,31 +79,43 @@ module EasyTalk
101
79
  # This method handles different types of properties:
102
80
  # - Nilable types (can be null)
103
81
  # - Types with dedicated builders
104
- # - Types that implement their own schema method
82
+ # - Types that implement their own schema method (EasyTalk models)
105
83
  # - Default fallback to 'object' type
106
84
  #
85
+ # When use_refs is enabled (globally or per-property), EasyTalk models
86
+ # are referenced via $ref instead of being inlined.
87
+ #
107
88
  # @return [Hash] The complete JSON Schema property definition
108
89
  #
109
90
  # @example Simple string property
110
91
  # property = Property.new(:name, 'String')
111
92
  # property.build # => {"type"=>"string"}
112
93
  #
113
- # @example Complex nested schema
94
+ # @example Complex nested schema (inlined)
114
95
  # address = Address.new # A class with a .schema method
115
96
  # property = Property.new(:shipping_address, address, description: "Shipping address")
116
97
  # property.build # => Address schema merged with the description constraint
98
+ #
99
+ # @example Nested schema with $ref
100
+ # property = Property.new(:shipping_address, Address, ref: true)
101
+ # property.build # => {"$ref"=>"#/$defs/Address", ...constraints}
117
102
  def build
118
103
  if nilable_type?
119
104
  build_nilable_schema
120
- elsif builder
121
- args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
122
- 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
123
111
  elsif type.respond_to?(:schema)
124
112
  # merge the top-level constraints from *this* property
125
113
  # e.g. :title, :description, :default, etc
126
114
  type.schema.merge!(constraints)
127
115
  else
128
- '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.'
129
119
  end
130
120
  end
131
121
 
@@ -142,28 +132,18 @@ module EasyTalk
142
132
  build.as_json
143
133
  end
144
134
 
145
- # Returns the builder class associated with the property type.
146
- #
147
- # The builder is responsible for converting the Ruby type to a JSON Schema type.
148
- #
149
- # @return [Class, nil] The builder class for this property's type, or nil if no dedicated builder exists
150
- # @see #find_builder_for_type
151
- def builder
152
- @builder ||= find_builder_for_type
153
- end
154
-
155
135
  private
156
136
 
157
- # Finds the appropriate builder for the current type.
137
+ # Finds the appropriate builder for the current type using the Builders::Registry.
158
138
  #
159
- # First checks if there's a builder for the class name, then falls back
160
- # 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.
161
141
  #
162
- # @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
163
143
  # @api private
144
+ # @see Builders::Registry.resolve
164
145
  def find_builder_for_type
165
- TYPE_TO_BUILDER[type.class.name.to_s] ||
166
- (type.respond_to?(:name) ? TYPE_TO_BUILDER[type.name.to_s] : nil)
146
+ Builders::Registry.resolve(type)
167
147
  end
168
148
 
169
149
  # Determines if the type is nilable (can be nil).
@@ -193,6 +173,14 @@ module EasyTalk
193
173
 
194
174
  return { type: 'null' } unless actual_type
195
175
 
176
+ # Check if the underlying type is an EasyTalk model that should use $ref
177
+ if RefHelper.should_use_ref_for_type?(actual_type, constraints)
178
+ # Use anyOf with $ref and null type
179
+ ref_constraints = constraints.except(:ref, :optional)
180
+ schema = { anyOf: [{ '$ref': actual_type.ref_template }, { type: 'null' }] }
181
+ return ref_constraints.empty? ? schema : schema.merge(ref_constraints)
182
+ end
183
+
196
184
  # Create a property with the actual type
197
185
  non_nil_schema = Property.new(name, actual_type, constraints).build
198
186
 
@@ -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
@@ -16,6 +16,10 @@ module EasyTalk
16
16
  self.class.name
17
17
  end
18
18
 
19
+ def to_s
20
+ name.to_s
21
+ end
22
+
19
23
  # Represents a composition type that allows all of the specified types.
20
24
  class AllOf < Composer
21
25
  def self.name