easy_talk 1.0.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/easy_talk.gemspec ADDED
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/easy_talk/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'easy_talk'
7
+ spec.version = EasyTalk::VERSION
8
+ spec.authors = ['Sergio Bayona']
9
+ spec.email = ['bayona.sergio@gmail.com']
10
+
11
+ spec.summary = 'Generate json-schema from Ruby classes with ActiveModel integration.'
12
+ spec.description = 'Generate json-schema from plain Ruby classes with ActiveModel integration for validations and serialization.'
13
+ spec.homepage = 'https://github.com/sergiobayona/easy_talk'
14
+ spec.license = 'MIT'
15
+ spec.required_ruby_version = '>= 3.2'
16
+
17
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
18
+
19
+ spec.metadata['homepage_uri'] = spec.homepage
20
+ spec.metadata['changelog_uri'] = 'https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md'
21
+
22
+ # Specify which files should be added to the gem when it is released.
23
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
24
+ spec.files = Dir.chdir(__dir__) do
25
+ `git ls-files -z`.split("\x0").reject do |f|
26
+ (File.expand_path(f) == __FILE__) ||
27
+ f.start_with?(*%w[bin/ spec/ .git .github Gemfile])
28
+ end
29
+ end
30
+
31
+ spec.require_paths = ['lib']
32
+
33
+ spec.add_dependency 'activemodel', ['>= 7.0', '< 9.0']
34
+ spec.add_dependency 'activesupport', ['>= 7.0', '< 9.0']
35
+ spec.add_dependency 'js_regex', '~> 3.0'
36
+ spec.add_dependency 'sorbet-runtime', '~> 0.5'
37
+
38
+ spec.metadata['rubygems_mfa_required'] = 'true'
39
+ end
@@ -50,7 +50,18 @@ module EasyTalk
50
50
  # @return [Array<Hash>] The array of schemas.
51
51
  def schemas
52
52
  items.map do |type|
53
- type.respond_to?(:schema) ? type.schema : { type: type.to_s.downcase }
53
+ if type.respond_to?(:schema)
54
+ type.schema
55
+ else
56
+ # Map Float type to 'number' in JSON Schema
57
+ json_type = case type.to_s
58
+ when 'Float', 'BigDecimal'
59
+ 'number'
60
+ else
61
+ type.to_s.downcase
62
+ end
63
+ { type: json_type }
64
+ end
54
65
  end
55
66
  end
56
67
 
@@ -7,6 +7,7 @@ module EasyTalk
7
7
  # Builder class for integer properties.
8
8
  class IntegerBuilder < BaseBuilder
9
9
  extend T::Sig
10
+
10
11
  VALID_OPTIONS = {
11
12
  minimum: { type: Integer, key: :minimum },
12
13
  maximum: { type: Integer, key: :maximum },
@@ -8,7 +8,7 @@ module EasyTalk
8
8
  # ObjectBuilder is responsible for turning a SchemaDefinition of an "object" type
9
9
  # into a validated JSON Schema hash. It:
10
10
  #
11
- # 1) Recursively processes the schemas :properties,
11
+ # 1) Recursively processes the schema's :properties,
12
12
  # 2) Determines which properties are required (unless optional),
13
13
  # 3) Handles sub-schema composition (allOf, anyOf, oneOf, not),
14
14
  # 4) Produces the final object-level schema hash.
@@ -55,7 +55,7 @@ module EasyTalk
55
55
 
56
56
  ##
57
57
  # Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
58
- # into a single hash that well feed to BaseBuilder.
58
+ # into a single hash that we'll feed to BaseBuilder.
59
59
  def build_options_hash
60
60
  # Start with a copy of the raw schema
61
61
  merged = @original_schema.dup
@@ -119,9 +119,7 @@ module EasyTalk
119
119
  return true if prop_options.dig(:constraints, :optional)
120
120
 
121
121
  # Check for nil_optional config to determine if nilable should also mean optional
122
- if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
123
- return EasyTalk.configuration.nilable_is_optional
124
- end
122
+ return EasyTalk.configuration.nilable_is_optional if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
125
123
 
126
124
  false
127
125
  end
@@ -134,24 +132,12 @@ module EasyTalk
134
132
  @property_cache ||= {}
135
133
 
136
134
  # Memoize so we only build each property once
137
- @property_cache[prop_name] ||= if prop_options[:properties]
138
- # This indicates block-style definition => nested schema
139
- nested_schema_builder(prop_options)
140
- else
141
- # Remove optional constraints from the property
142
- constraints = prop_options[:constraints].except(:optional)
143
- # Normal property: e.g. { type: String, constraints: {...} }
144
- Property.new(prop_name, prop_options[:type], constraints)
145
- end
146
- end
147
-
148
- ##
149
- # Build a child schema by calling another ObjectBuilder on the nested SchemaDefinition.
150
- #
151
- def nested_schema_builder(prop_options)
152
- child_schema_def = prop_options[:properties]
153
- # If user used T.nilable(...) with a block, unwrap the nilable
154
- ObjectBuilder.new(child_schema_def).build
135
+ @property_cache[prop_name] ||= begin
136
+ # Remove optional constraints from the property
137
+ constraints = prop_options[:constraints].except(:optional)
138
+ # Normal property: e.g. { type: String, constraints: {...} }
139
+ Property.new(prop_name, prop_options[:type], constraints)
140
+ end
155
141
  end
156
142
 
157
143
  ##
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative 'base_builder'
4
+ require 'js_regex' # Compile the ruby regex to JS regex
4
5
  require 'sorbet-runtime' # Add the import statement for the T module
5
6
 
6
7
  module EasyTalk
@@ -8,6 +9,7 @@ module EasyTalk
8
9
  # Builder class for string properties.
9
10
  class StringBuilder < BaseBuilder
10
11
  extend T::Sig
12
+
11
13
  VALID_OPTIONS = {
12
14
  format: { type: String, key: :format },
13
15
  pattern: { type: String, key: :pattern },
@@ -22,6 +24,13 @@ module EasyTalk
22
24
  def initialize(name, constraints = {})
23
25
  super(name, { type: 'string' }, constraints, VALID_OPTIONS)
24
26
  end
27
+
28
+ def build
29
+ super.tap do |schema|
30
+ pattern = schema[:pattern]
31
+ schema[:pattern] = JsRegex.new(pattern).source if pattern.is_a?(String)
32
+ end
33
+ end
25
34
  end
26
35
  end
27
36
  end
@@ -2,18 +2,12 @@
2
2
 
3
3
  module EasyTalk
4
4
  class Configuration
5
- attr_accessor :exclude_foreign_keys, :exclude_associations, :excluded_columns,
6
- :exclude_primary_key, :exclude_timestamps, :default_additional_properties,
7
- :nilable_is_optional
5
+ attr_accessor :default_additional_properties, :nilable_is_optional, :auto_validations
8
6
 
9
7
  def initialize
10
- @exclude_foreign_keys = true
11
- @exclude_associations = true
12
- @excluded_columns = []
13
- @exclude_primary_key = true # New option, defaulting to true
14
- @exclude_timestamps = true # New option, defaulting to true
15
8
  @default_additional_properties = false
16
9
  @nilable_is_optional = false
10
+ @auto_validations = true
17
11
  end
18
12
  end
19
13
 
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EasyTalk
4
+ # Helper module for generating consistent error messages
4
5
  module ErrorHelper
5
6
  def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
6
7
  message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
@@ -9,7 +9,7 @@ require 'active_support/json'
9
9
  require 'active_model'
10
10
  require_relative 'builders/object_builder'
11
11
  require_relative 'schema_definition'
12
- require_relative 'active_record_schema_builder'
12
+ require_relative 'validation_builder'
13
13
 
14
14
  module EasyTalk
15
15
  # The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
@@ -35,22 +35,43 @@ module EasyTalk
35
35
  # @see SchemaDefinition
36
36
  module Model
37
37
  def self.included(base)
38
- base.include ActiveModel::API # Include ActiveModel::API in the class including EasyTalk::Model
38
+ base.extend(ClassMethods)
39
+
40
+ base.include ActiveModel::API
39
41
  base.include ActiveModel::Validations
40
42
  base.extend ActiveModel::Callbacks
41
- base.extend(ClassMethods)
42
43
  base.include(InstanceMethods)
43
-
44
- # Apply ActiveRecord-specific functionality if appropriate
45
- return unless defined?(ActiveRecord) && base.ancestors.include?(ActiveRecord::Base)
46
-
47
- base.extend(ActiveRecordClassMethods)
48
44
  end
49
45
 
46
+ # Instance methods mixed into models that include EasyTalk::Model
50
47
  module InstanceMethods
51
48
  def initialize(attributes = {})
52
49
  @additional_properties = {}
53
- super
50
+ super # Perform initial mass assignment
51
+
52
+ # After initial assignment, instantiate nested EasyTalk::Model objects
53
+ schema_def = self.class.schema_definition
54
+
55
+ # Only proceed if we have a valid schema definition
56
+ return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
57
+
58
+ (schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
59
+ # Get the defined type and the currently assigned value
60
+ defined_type = prop_definition[:type]
61
+ current_value = public_send(prop_name)
62
+ nilable_type = defined_type.respond_to?(:nilable?) && defined_type.nilable?
63
+
64
+ next if nilable_type && current_value.nil?
65
+
66
+ defined_type = T::Utils::Nilable.get_underlying_type(defined_type) if nilable_type
67
+
68
+ # Check if the type is another EasyTalk::Model and the value is a Hash
69
+ next unless defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
70
+
71
+ # Instantiate the nested model and assign it back
72
+ nested_instance = defined_type.new(current_value)
73
+ public_send("#{prop_name}=", nested_instance)
74
+ end
54
75
  end
55
76
 
56
77
  def method_missing(method_name, *args)
@@ -77,9 +98,10 @@ module EasyTalk
77
98
 
78
99
  # Add to_hash method to convert defined properties to hash
79
100
  def to_hash
80
- return {} unless self.class.properties
101
+ properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
102
+ return {} if properties_to_include.empty?
81
103
 
82
- self.class.properties.each_with_object({}) do |prop, hash|
104
+ properties_to_include.each_with_object({}) do |prop, hash|
83
105
  hash[prop.to_s] = send(prop)
84
106
  end
85
107
  end
@@ -88,6 +110,28 @@ module EasyTalk
88
110
  def as_json(_options = {})
89
111
  to_hash.merge(@additional_properties)
90
112
  end
113
+
114
+ # to_h includes both defined and additional properties
115
+ def to_h
116
+ to_hash.merge(@additional_properties)
117
+ end
118
+
119
+ # Allow comparison with hashes
120
+ def ==(other)
121
+ case other
122
+ when Hash
123
+ # Convert both to comparable format for comparison
124
+ self_hash = (self.class.schema_definition.schema[:properties] || {}).keys.each_with_object({}) do |prop, hash|
125
+ hash[prop] = send(prop)
126
+ end
127
+
128
+ # Handle both symbol and string keys in the other hash
129
+ other_normalized = other.transform_keys(&:to_sym)
130
+ self_hash == other_normalized
131
+ else
132
+ super
133
+ end
134
+ end
91
135
  end
92
136
 
93
137
  # Module containing class-level methods for defining and accessing the schema of a model.
@@ -97,13 +141,8 @@ module EasyTalk
97
141
  # @return [Schema] The schema for the model.
98
142
  def schema
99
143
  @schema ||= if defined?(@schema_definition) && @schema_definition
100
- # Schema defined explicitly via define_schema
101
144
  build_schema(@schema_definition)
102
- elsif respond_to?(:active_record_schema_definition)
103
- # ActiveRecord model without explicit schema definition
104
- build_schema(active_record_schema_definition)
105
145
  else
106
- # Default case - empty schema
107
146
  {}
108
147
  end
109
148
  end
@@ -115,14 +154,6 @@ module EasyTalk
115
154
  "#/$defs/#{name}"
116
155
  end
117
156
 
118
- def properties
119
- @properties ||= begin
120
- return unless schema[:properties].present?
121
-
122
- schema[:properties].keys.map(&:to_sym)
123
- end
124
- end
125
-
126
157
  # Returns the JSON schema for the model.
127
158
  #
128
159
  # @return [Hash] The JSON schema for the model.
@@ -134,12 +165,30 @@ module EasyTalk
134
165
  #
135
166
  # @yield The block to define the schema.
136
167
  # @raise [ArgumentError] If the class does not have a name.
137
- def define_schema(&block)
168
+ def define_schema(&)
138
169
  raise ArgumentError, 'The class must have a name' unless name.present?
139
170
 
140
171
  @schema_definition = SchemaDefinition.new(name)
141
- @schema_definition.instance_eval(&block)
142
- attr_accessor(*properties)
172
+ @schema_definition.klass = self # Pass the model class to the schema definition
173
+ @schema_definition.instance_eval(&)
174
+
175
+ # Define accessors immediately based on schema_definition
176
+ defined_properties = (@schema_definition.schema[:properties] || {}).keys
177
+ attr_accessor(*defined_properties)
178
+
179
+ # Track which properties have had validations applied
180
+ @validated_properties ||= Set.new
181
+
182
+ # Apply auto-validations immediately after definition
183
+ if EasyTalk.configuration.auto_validations
184
+ (@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
185
+ # Only apply validations if they haven't been applied yet
186
+ unless @validated_properties.include?(prop_name)
187
+ ValidationBuilder.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
188
+ @validated_properties.add(prop_name)
189
+ end
190
+ end
191
+ end
143
192
 
144
193
  @schema_definition
145
194
  end
@@ -155,6 +204,13 @@ module EasyTalk
155
204
  @schema_definition&.schema&.fetch(:additional_properties, false)
156
205
  end
157
206
 
207
+ # Returns the property names defined in the schema
208
+ #
209
+ # @return [Array<Symbol>] Array of property names as symbols
210
+ def properties
211
+ (@schema_definition&.schema&.dig(:properties) || {}).keys
212
+ end
213
+
158
214
  # Builds the schema using the provided schema definition.
159
215
  # This is the convergence point for all schema generation.
160
216
  #
@@ -164,34 +220,5 @@ module EasyTalk
164
220
  Builders::ObjectBuilder.new(schema_definition).build
165
221
  end
166
222
  end
167
-
168
- # Module containing ActiveRecord-specific methods for schema generation
169
- module ActiveRecordClassMethods
170
- # Gets a SchemaDefinition that's built from the ActiveRecord database schema
171
- #
172
- # @return [SchemaDefinition] A schema definition built from the database
173
- def active_record_schema_definition
174
- @active_record_schema_definition ||= ActiveRecordSchemaBuilder.new(self).build_schema_definition
175
- end
176
-
177
- # Store enhancements to be applied to the schema
178
- #
179
- # @return [Hash] The schema enhancements
180
- def schema_enhancements
181
- @schema_enhancements ||= {}
182
- end
183
-
184
- # Enhance the generated schema with additional information
185
- #
186
- # @param enhancements [Hash] The schema enhancements
187
- # @return [void]
188
- def enhance_schema(enhancements)
189
- @schema_enhancements = enhancements
190
- # Clear cached values to force regeneration
191
- @active_record_schema_definition = nil
192
- @schema = nil
193
- @json_schema = nil
194
- end
195
- end
196
223
  end
197
224
  end
@@ -11,30 +11,51 @@ require_relative 'builders/composition_builder'
11
11
  require_relative 'builders/typed_array_builder'
12
12
  require_relative 'builders/union_builder'
13
13
 
14
- # frozen_string_literal: true
15
-
16
- # EasyTalk module provides classes for building JSON schema properties.
14
+ # EasyTalk module provides a DSL for building JSON Schema definitions.
15
+ #
16
+ # This module contains classes and utilities for easily creating valid JSON Schema
17
+ # documents with a Ruby-native syntax. The `Property` class serves as the main entry
18
+ # point for defining schema properties.
17
19
  #
18
- # This module contains the `Property` class, which is used to build a JSON schema property.
19
- # It also defines a constant `TYPE_TO_BUILDER` which maps property types to their respective builders.
20
+ # @example Basic property definition
21
+ # property = EasyTalk::Property.new(:name, String, minLength: 3, maxLength: 50)
22
+ # property.build # => {"type"=>"string", "minLength"=>3, "maxLength"=>50}
20
23
  #
21
- # Example usage:
22
- # property = EasyTalk::Property.new(:name, 'String', minLength: 3, maxLength: 50)
23
- # property.build
24
+ # @example Using with nilable types
25
+ # nilable_prop = EasyTalk::Property.new(:optional_field, T::Types::Union.new(String, NilClass))
26
+ # nilable_prop.build # => {"type"=>["string", "null"]}
24
27
  #
25
28
  # @see EasyTalk::Property
29
+ # @see https://json-schema.org/ JSON Schema Documentation
26
30
  module EasyTalk
27
31
  # Property class for building a JSON schema property.
32
+ #
33
+ # This class handles the conversion from Ruby types to JSON Schema property definitions,
34
+ # and provides support for common constraints like minimum/maximum values, string patterns,
35
+ # and custom validators.
28
36
  class Property
29
37
  extend T::Sig
30
- attr_reader :name, :type, :constraints
31
38
 
39
+ # @return [Symbol] The name of the property
40
+ attr_reader :name
41
+
42
+ # @return [Object] The type definition of the property
43
+ attr_reader :type
44
+
45
+ # @return [Hash<Symbol, Object>] Additional constraints applied to the property
46
+ attr_reader :constraints
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
32
52
  TYPE_TO_BUILDER = {
33
53
  'String' => Builders::StringBuilder,
34
54
  'Integer' => Builders::IntegerBuilder,
35
55
  'Float' => Builders::NumberBuilder,
36
56
  'BigDecimal' => Builders::NumberBuilder,
37
57
  'T::Boolean' => Builders::BooleanBuilder,
58
+ 'TrueClass' => Builders::BooleanBuilder,
38
59
  'NilClass' => Builders::NullBuilder,
39
60
  'Date' => Builders::TemporalBuilder::DateBuilder,
40
61
  'DateTime' => Builders::TemporalBuilder::DatetimeBuilder,
@@ -47,10 +68,19 @@ module EasyTalk
47
68
  }.freeze
48
69
 
49
70
  # Initializes a new instance of the Property class.
50
- # @param name [Symbol] The name of the property.
51
- # @param type [Object] The type of the property.
52
- # @param constraints [Hash] The property constraints.
53
- # @raise [ArgumentError] If the property type is missing.
71
+ #
72
+ # @param name [Symbol] The name of the property
73
+ # @param type [Object] The type of the property (Ruby class, string name, or Sorbet type)
74
+ # @param constraints [Hash<Symbol, Object>] Additional constraints for the property
75
+ # (e.g., minLength, pattern, format)
76
+ #
77
+ # @example String property with constraints
78
+ # Property.new(:username, 'String', minLength: 3, maxLength: 20, pattern: '^[a-z0-9_]+$')
79
+ #
80
+ # @example Integer property with range
81
+ # Property.new(:age, 'Integer', minimum: 0, maximum: 120)
82
+ #
83
+ # @raise [ArgumentError] If the property type is missing or empty
54
84
  sig do
55
85
  params(name: Symbol, type: T.any(String, Object),
56
86
  constraints: T::Hash[Symbol, T.untyped]).void
@@ -59,18 +89,31 @@ module EasyTalk
59
89
  @name = name
60
90
  @type = type
61
91
  @constraints = constraints
62
- raise ArgumentError, 'property type is missing' if type.blank?
92
+ if type.nil? || (type.respond_to?(:empty?) && type.is_a?(String) && type.strip.empty?)
93
+ raise ArgumentError,
94
+ 'property type is missing'
95
+ end
96
+ raise ArgumentError, 'property type is not supported' if type.is_a?(Array) && type.empty?
63
97
  end
64
98
 
65
- # Builds the property based on the specified type, constraints, and builder.
99
+ # Builds the property schema based on its type and constraints.
66
100
  #
67
- # If the type responds to the `schema` method, it returns the schema of the type.
68
- # Otherwise, it returns 'object'.
101
+ # This method handles different types of properties:
102
+ # - Nilable types (can be null)
103
+ # - Types with dedicated builders
104
+ # - Types that implement their own schema method
105
+ # - Default fallback to 'object' type
69
106
  #
70
- # If a builder is specified, it uses the builder to build the property.
71
- # The arguments passed to the builder depend on whether the builder is a collection type or not.
107
+ # @return [Hash] The complete JSON Schema property definition
72
108
  #
73
- # @return [Object] The built property.
109
+ # @example Simple string property
110
+ # property = Property.new(:name, 'String')
111
+ # property.build # => {"type"=>"string"}
112
+ #
113
+ # @example Complex nested schema
114
+ # address = Address.new # A class with a .schema method
115
+ # property = Property.new(:shipping_address, address, description: "Shipping address")
116
+ # property.build # => Address schema merged with the description constraint
74
117
  def build
75
118
  if nilable_type?
76
119
  build_nilable_schema
@@ -86,43 +129,76 @@ module EasyTalk
86
129
  end
87
130
  end
88
131
 
89
- # Converts the object to a JSON representation.
132
+ # Converts the property definition to a JSON-compatible format.
133
+ #
134
+ # This method enables seamless integration with Ruby's JSON library.
135
+ #
136
+ # @param _args [Array] Optional arguments passed to #as_json (ignored)
137
+ # @return [Hash] The JSON-compatible representation of the property schema
90
138
  #
91
- # @param _args [Array] Optional arguments
92
- # @return [Hash] The JSON representation of the object
139
+ # @see #build
140
+ # @see https://ruby-doc.org/stdlib-2.7.2/libdoc/json/rdoc/JSON.html#as_json-method
93
141
  def as_json(*_args)
94
142
  build.as_json
95
143
  end
96
144
 
97
- # Returns the builder associated with the property type.
145
+ # Returns the builder class associated with the property type.
98
146
  #
99
- # The builder is responsible for constructing the property based on its type.
100
- # It looks up the builder based on the type's class name or name.
147
+ # The builder is responsible for converting the Ruby type to a JSON Schema type.
101
148
  #
102
- # @return [Builder] The builder associated with the property type.
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
103
151
  def builder
104
- @builder ||= TYPE_TO_BUILDER[type.class.name.to_s] || TYPE_TO_BUILDER[type.name.to_s]
152
+ @builder ||= find_builder_for_type
105
153
  end
106
154
 
107
155
  private
108
156
 
157
+ # Finds the appropriate builder for the current type.
158
+ #
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).
161
+ #
162
+ # @return [Class, nil] The builder class for this type, or nil if none matches
163
+ # @api private
164
+ 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)
167
+ end
168
+
169
+ # Determines if the type is nilable (can be nil).
170
+ #
171
+ # A type is nilable if it's a union type that includes NilClass.
172
+ # This is typically represented as T.nilable(Type) in Sorbet.
173
+ #
174
+ # @return [Boolean] true if the type is nilable, false otherwise
175
+ # @api private
109
176
  def nilable_type?
110
- return unless type.respond_to?(:types)
111
- return unless type.types.all? { |t| t.respond_to?(:raw_type) }
177
+ return false unless type.respond_to?(:types)
178
+ return false unless type.types.all? { |t| t.respond_to?(:raw_type) }
112
179
 
113
180
  type.types.any? { |t| t.raw_type == NilClass }
114
181
  end
115
182
 
183
+ # Builds a schema for a nilable type, which can be either the actual type or null.
184
+ #
185
+ # @return [Hash] A schema with both the actual type and null type
186
+ # @api private
187
+ # @example
188
+ # # For a T.nilable(String) type:
189
+ # {"type"=>["string", "null"]}
116
190
  def build_nilable_schema
117
191
  # Extract the non-nil type from the Union
118
- actual_type = type.types.find { |t| t != NilClass }
192
+ actual_type = T::Utils::Nilable.get_underlying_type(type)
193
+
194
+ return { type: 'null' } unless actual_type
119
195
 
120
196
  # Create a property with the actual type
121
197
  non_nil_schema = Property.new(name, actual_type, constraints).build
122
198
 
123
199
  # Merge the types into an array
124
200
  non_nil_schema.merge(
125
- type: [non_nil_schema[:type], 'null']
201
+ type: [non_nil_schema[:type], 'null'].compact
126
202
  )
127
203
  end
128
204
  end