easy_talk 1.0.3 → 2.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,38 @@
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.'
12
+ spec.description = 'Generate json-schema from plain Ruby classes.'
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'
34
+ spec.add_dependency 'activesupport', '~> 7.0'
35
+ spec.add_dependency 'sorbet-runtime', '~> 0.5'
36
+
37
+ spec.metadata['rubygems_mfa_required'] = 'true'
38
+ end
@@ -98,6 +98,9 @@ module EasyTalk
98
98
  # Map the database type to Ruby type
99
99
  ruby_type = COLUMN_TYPE_MAP.fetch(column.type, String)
100
100
 
101
+ # If the column is nullable, wrap the type in a Union with NilClass
102
+ ruby_type = T::Types::Union.new([ruby_type, NilClass]) if column.null
103
+
101
104
  # Build constraints hash for this column
102
105
  constraints = build_column_constraints(column, column_enhancements)
103
106
 
@@ -133,7 +136,11 @@ module EasyTalk
133
136
  end
134
137
 
135
138
  # Add default value if present and not a proc
136
- constraints[:default] = column.default if column.default && !column.default.is_a?(Proc)
139
+ if column.default && !column.default.is_a?(Proc) && column.type == :boolean
140
+ constraints[:default] = ActiveModel::Type::Boolean.new.cast(column.default)
141
+ elsif column.default && !column.default.is_a?(Proc)
142
+ constraints[:default] = column.default
143
+ end
137
144
 
138
145
  # Remove nil values
139
146
  constraints.compact
@@ -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
 
@@ -60,6 +71,18 @@ module EasyTalk
60
71
  def items
61
72
  @type.items
62
73
  end
74
+
75
+ # Builder class for AllOf composition.
76
+ class AllOfBuilder < CompositionBuilder
77
+ end
78
+
79
+ # Builder class for AnyOf composition.
80
+ class AnyOfBuilder < CompositionBuilder
81
+ end
82
+
83
+ # Builder class for OneOf composition.
84
+ class OneOfBuilder < CompositionBuilder
85
+ end
63
86
  end
64
87
  end
65
88
  end
@@ -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
@@ -118,6 +118,9 @@ module EasyTalk
118
118
  # Check constraints[:optional]
119
119
  return true if prop_options.dig(:constraints, :optional)
120
120
 
121
+ # Check for nil_optional config to determine if nilable should also mean optional
122
+ return EasyTalk.configuration.nilable_is_optional if prop_options[:type].respond_to?(:nilable?) && prop_options[:type].nilable?
123
+
121
124
  false
122
125
  end
123
126
 
@@ -129,24 +132,12 @@ module EasyTalk
129
132
  @property_cache ||= {}
130
133
 
131
134
  # Memoize so we only build each property once
132
- @property_cache[prop_name] ||= if prop_options[:properties]
133
- # This indicates block-style definition => nested schema
134
- nested_schema_builder(prop_options)
135
- else
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
141
- end
142
-
143
- ##
144
- # Build a child schema by calling another ObjectBuilder on the nested SchemaDefinition.
145
- #
146
- def nested_schema_builder(prop_options)
147
- child_schema_def = prop_options[:properties]
148
- # If user used T.nilable(...) with a block, unwrap the nilable
149
- 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
150
141
  end
151
142
 
152
143
  ##
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'string_builder'
4
+
5
+ module EasyTalk
6
+ module Builders
7
+ # Builder class for temporal properties (date, datetime, time).
8
+ class TemporalBuilder < StringBuilder
9
+ # Initializes a new instance of the TemporalBuilder class.
10
+ #
11
+ # @param property_name [Symbol] The name of the property.
12
+ # @param options [Hash] The options for the builder.
13
+ # @param format [String] The format of the temporal property (date, date-time, time).
14
+ def initialize(property_name, options = {}, format = nil)
15
+ super(property_name, options)
16
+ @format = format
17
+ end
18
+
19
+ # Modifies the schema to include the format constraint for a temporal property.
20
+ sig { returns(T::Hash[Symbol, T.untyped]) }
21
+ def schema
22
+ super.tap do |schema|
23
+ schema[:format] = @format if @format
24
+ end
25
+ end
26
+
27
+ # Builder class for date properties.
28
+ class DateBuilder < TemporalBuilder
29
+ def initialize(property_name, options = {})
30
+ super(property_name, options, 'date')
31
+ end
32
+ end
33
+
34
+ # Builder class for datetime properties.
35
+ class DatetimeBuilder < TemporalBuilder
36
+ def initialize(property_name, options = {})
37
+ super(property_name, options, 'date-time')
38
+ end
39
+ end
40
+
41
+ # Builder class for time properties.
42
+ class TimeBuilder < TemporalBuilder
43
+ def initialize(property_name, options = {})
44
+ super(property_name, options, 'time')
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -3,15 +3,18 @@
3
3
  module EasyTalk
4
4
  class Configuration
5
5
  attr_accessor :exclude_foreign_keys, :exclude_associations, :excluded_columns,
6
- :exclude_primary_key, :exclude_timestamps, :default_additional_properties
6
+ :exclude_primary_key, :exclude_timestamps, :default_additional_properties,
7
+ :nilable_is_optional, :auto_validations
7
8
 
8
9
  def initialize
9
10
  @exclude_foreign_keys = true
10
11
  @exclude_associations = true
11
12
  @excluded_columns = []
12
- @exclude_primary_key = true # New option, defaulting to true
13
- @exclude_timestamps = true # New option, defaulting to true
13
+ @exclude_primary_key = true
14
+ @exclude_timestamps = true
14
15
  @default_additional_properties = false
16
+ @nilable_is_optional = false
17
+ @auto_validations = true # New option: enable validations by default
15
18
  end
16
19
  end
17
20
 
@@ -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}, " \
@@ -10,6 +10,7 @@ require 'active_model'
10
10
  require_relative 'builders/object_builder'
11
11
  require_relative 'schema_definition'
12
12
  require_relative 'active_record_schema_builder'
13
+ require_relative 'validation_builder'
13
14
 
14
15
  module EasyTalk
15
16
  # The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
@@ -47,10 +48,35 @@ module EasyTalk
47
48
  base.extend(ActiveRecordClassMethods)
48
49
  end
49
50
 
51
+ # Instance methods mixed into models that include EasyTalk::Model
50
52
  module InstanceMethods
51
53
  def initialize(attributes = {})
52
54
  @additional_properties = {}
53
- super
55
+ super # Perform initial mass assignment
56
+
57
+ # After initial assignment, instantiate nested EasyTalk::Model objects
58
+ # Get the appropriate schema definition based on model type
59
+ schema_def = if self.class.respond_to?(:active_record_schema_definition)
60
+ self.class.active_record_schema_definition
61
+ else
62
+ self.class.schema_definition
63
+ end
64
+
65
+ # Only proceed if we have a valid schema definition
66
+ return unless schema_def.respond_to?(:schema) && schema_def.schema.is_a?(Hash)
67
+
68
+ (schema_def.schema[:properties] || {}).each do |prop_name, prop_definition|
69
+ # Get the defined type and the currently assigned value
70
+ defined_type = prop_definition[:type]
71
+ current_value = public_send(prop_name)
72
+
73
+ # Check if the type is another EasyTalk::Model and the value is a Hash
74
+ next unless defined_type.is_a?(Class) && defined_type.include?(EasyTalk::Model) && current_value.is_a?(Hash)
75
+
76
+ # Instantiate the nested model and assign it back
77
+ nested_instance = defined_type.new(current_value)
78
+ public_send("#{prop_name}=", nested_instance)
79
+ end
54
80
  end
55
81
 
56
82
  def method_missing(method_name, *args)
@@ -77,9 +103,10 @@ module EasyTalk
77
103
 
78
104
  # Add to_hash method to convert defined properties to hash
79
105
  def to_hash
80
- return {} unless self.class.properties
106
+ properties_to_include = (self.class.schema_definition.schema[:properties] || {}).keys
107
+ return {} if properties_to_include.empty?
81
108
 
82
- self.class.properties.each_with_object({}) do |prop, hash|
109
+ properties_to_include.each_with_object({}) do |prop, hash|
83
110
  hash[prop.to_s] = send(prop)
84
111
  end
85
112
  end
@@ -88,6 +115,23 @@ module EasyTalk
88
115
  def as_json(_options = {})
89
116
  to_hash.merge(@additional_properties)
90
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.
@@ -115,14 +159,6 @@ module EasyTalk
115
159
  "#/$defs/#{name}"
116
160
  end
117
161
 
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
162
  # Returns the JSON schema for the model.
127
163
  #
128
164
  # @return [Hash] The JSON schema for the model.
@@ -134,12 +170,30 @@ module EasyTalk
134
170
  #
135
171
  # @yield The block to define the schema.
136
172
  # @raise [ArgumentError] If the class does not have a name.
137
- def define_schema(&block)
173
+ def define_schema(&)
138
174
  raise ArgumentError, 'The class must have a name' unless name.present?
139
175
 
140
176
  @schema_definition = SchemaDefinition.new(name)
141
- @schema_definition.instance_eval(&block)
142
- attr_accessor(*properties)
177
+ @schema_definition.klass = self # Pass the model class to the schema definition
178
+ @schema_definition.instance_eval(&)
179
+
180
+ # Define accessors immediately based on schema_definition
181
+ defined_properties = (@schema_definition.schema[:properties] || {}).keys
182
+ attr_accessor(*defined_properties)
183
+
184
+ # Track which properties have had validations applied
185
+ @validated_properties ||= Set.new
186
+
187
+ # Apply auto-validations immediately after definition
188
+ if EasyTalk.configuration.auto_validations
189
+ (@schema_definition.schema[:properties] || {}).each do |prop_name, prop_def|
190
+ # Only apply validations if they haven't been applied yet
191
+ unless @validated_properties.include?(prop_name)
192
+ ValidationBuilder.build_validations(self, prop_name, prop_def[:type], prop_def[:constraints])
193
+ @validated_properties.add(prop_name)
194
+ end
195
+ end
196
+ end
143
197
 
144
198
  @schema_definition
145
199
  end
@@ -155,6 +209,13 @@ module EasyTalk
155
209
  @schema_definition&.schema&.fetch(:additional_properties, false)
156
210
  end
157
211
 
212
+ # Returns the property names defined in the schema
213
+ #
214
+ # @return [Array<Symbol>] Array of property names as symbols
215
+ def properties
216
+ (@schema_definition&.schema&.dig(:properties) || {}).keys
217
+ end
218
+
158
219
  # Builds the schema using the provided schema definition.
159
220
  # This is the convergence point for all schema generation.
160
221
  #
@@ -6,55 +6,81 @@ require_relative 'builders/number_builder'
6
6
  require_relative 'builders/boolean_builder'
7
7
  require_relative 'builders/null_builder'
8
8
  require_relative 'builders/string_builder'
9
- require_relative 'builders/date_builder'
10
- require_relative 'builders/datetime_builder'
11
- require_relative 'builders/time_builder'
12
- require_relative 'builders/any_of_builder'
13
- require_relative 'builders/all_of_builder'
14
- require_relative 'builders/one_of_builder'
9
+ require_relative 'builders/temporal_builder'
10
+ require_relative 'builders/composition_builder'
15
11
  require_relative 'builders/typed_array_builder'
16
12
  require_relative 'builders/union_builder'
17
13
 
18
- # frozen_string_literal: true
19
-
20
- # 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.
21
19
  #
22
- # This module contains the `Property` class, which is used to build a JSON schema property.
23
- # 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}
24
23
  #
25
- # Example usage:
26
- # property = EasyTalk::Property.new(:name, 'String', minLength: 3, maxLength: 50)
27
- # 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"]}
28
27
  #
29
28
  # @see EasyTalk::Property
29
+ # @see https://json-schema.org/ JSON Schema Documentation
30
30
  module EasyTalk
31
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.
32
36
  class Property
33
37
  extend T::Sig
34
- attr_reader :name, :type, :constraints
35
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
36
52
  TYPE_TO_BUILDER = {
37
53
  'String' => Builders::StringBuilder,
38
54
  'Integer' => Builders::IntegerBuilder,
39
55
  'Float' => Builders::NumberBuilder,
40
56
  'BigDecimal' => Builders::NumberBuilder,
41
57
  'T::Boolean' => Builders::BooleanBuilder,
58
+ 'TrueClass' => Builders::BooleanBuilder,
42
59
  'NilClass' => Builders::NullBuilder,
43
- 'Date' => Builders::DateBuilder,
44
- 'DateTime' => Builders::DatetimeBuilder,
45
- 'Time' => Builders::TimeBuilder,
46
- 'anyOf' => Builders::AnyOfBuilder,
47
- 'allOf' => Builders::AllOfBuilder,
48
- 'oneOf' => Builders::OneOfBuilder,
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,
49
66
  'T::Types::TypedArray' => Builders::TypedArrayBuilder,
50
67
  'T::Types::Union' => Builders::UnionBuilder
51
68
  }.freeze
52
69
 
53
70
  # Initializes a new instance of the Property class.
54
- # @param name [Symbol] The name of the property.
55
- # @param type [Object] The type of the property.
56
- # @param constraints [Hash] The property constraints.
57
- # @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
58
84
  sig do
59
85
  params(name: Symbol, type: T.any(String, Object),
60
86
  constraints: T::Hash[Symbol, T.untyped]).void
@@ -63,18 +89,31 @@ module EasyTalk
63
89
  @name = name
64
90
  @type = type
65
91
  @constraints = constraints
66
- 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?
67
97
  end
68
98
 
69
- # Builds the property based on the specified type, constraints, and builder.
99
+ # Builds the property schema based on its type and constraints.
70
100
  #
71
- # If the type responds to the `schema` method, it returns the schema of the type.
72
- # 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
73
106
  #
74
- # If a builder is specified, it uses the builder to build the property.
75
- # 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
76
108
  #
77
- # @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
78
117
  def build
79
118
  if nilable_type?
80
119
  build_nilable_schema
@@ -90,43 +129,76 @@ module EasyTalk
90
129
  end
91
130
  end
92
131
 
93
- # 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
94
138
  #
95
- # @param _args [Array] Optional arguments
96
- # @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
97
141
  def as_json(*_args)
98
142
  build.as_json
99
143
  end
100
144
 
101
- # Returns the builder associated with the property type.
145
+ # Returns the builder class associated with the property type.
102
146
  #
103
- # The builder is responsible for constructing the property based on its type.
104
- # 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.
105
148
  #
106
- # @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
107
151
  def builder
108
- @builder ||= TYPE_TO_BUILDER[type.class.name.to_s] || TYPE_TO_BUILDER[type.name.to_s]
152
+ @builder ||= find_builder_for_type
109
153
  end
110
154
 
111
155
  private
112
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
113
176
  def nilable_type?
114
- return unless type.respond_to?(:types)
115
- 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) }
116
179
 
117
180
  type.types.any? { |t| t.raw_type == NilClass }
118
181
  end
119
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"]}
120
190
  def build_nilable_schema
121
191
  # Extract the non-nil type from the Union
122
- actual_type = type.types.find { |t| t != NilClass }
192
+ actual_type = type.types.find { |t| t.raw_type != NilClass }
193
+
194
+ return { type: 'null' } unless actual_type
123
195
 
124
196
  # Create a property with the actual type
125
197
  non_nil_schema = Property.new(name, actual_type, constraints).build
126
198
 
127
199
  # Merge the types into an array
128
200
  non_nil_schema.merge(
129
- type: [non_nil_schema[:type], 'null']
201
+ type: [non_nil_schema[:type], 'null'].compact
130
202
  )
131
203
  end
132
204
  end