easy_talk 1.0.2 → 1.0.4

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.
@@ -0,0 +1,295 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ # This class is responsible for building a SchemaDefinition from an ActiveRecord model
5
+ # It analyzes the database schema and creates a SchemaDefinition that can be
6
+ # passed to ObjectBuilder for final schema generation
7
+ class ActiveRecordSchemaBuilder
8
+ # Mapping of ActiveRecord column types to Ruby classes
9
+ COLUMN_TYPE_MAP = {
10
+ string: String,
11
+ text: String,
12
+ integer: Integer,
13
+ bigint: Integer,
14
+ float: Float,
15
+ decimal: Float,
16
+ boolean: T::Boolean,
17
+ date: Date,
18
+ datetime: DateTime,
19
+ timestamp: DateTime,
20
+ time: Time,
21
+ json: Hash,
22
+ jsonb: Hash
23
+ }.freeze
24
+
25
+ # Mapping for format constraints based on column type
26
+ FORMAT_MAP = {
27
+ date: 'date',
28
+ datetime: 'date-time',
29
+ timestamp: 'date-time',
30
+ time: 'time'
31
+ }.freeze
32
+
33
+ attr_reader :model
34
+
35
+ # Initialize the builder with an ActiveRecord model
36
+ #
37
+ # @param model [Class] An ActiveRecord model class
38
+ # @raise [ArgumentError] If the provided class is not an ActiveRecord model
39
+ def initialize(model)
40
+ raise ArgumentError, 'Class must be an ActiveRecord model' unless model.ancestors.include?(ActiveRecord::Base)
41
+
42
+ @model = model
43
+ end
44
+
45
+ # Build a SchemaDefinition object from the ActiveRecord model
46
+ #
47
+ # @return [EasyTalk::SchemaDefinition] A schema definition built from the database structure
48
+ def build_schema_definition
49
+ schema_def = SchemaDefinition.new(model.name)
50
+
51
+ # Apply basic schema metadata
52
+ apply_schema_metadata(schema_def)
53
+
54
+ # Add all database columns as properties
55
+ add_column_properties(schema_def)
56
+
57
+ # Add model associations as properties
58
+ add_association_properties(schema_def) unless EasyTalk.configuration.exclude_associations
59
+
60
+ # Add virtual properties defined in schema_enhancements
61
+ add_virtual_properties(schema_def)
62
+
63
+ schema_def
64
+ end
65
+
66
+ private
67
+
68
+ # Set top-level schema metadata like title, description, and additionalProperties
69
+ #
70
+ # @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
71
+ def apply_schema_metadata(schema_def)
72
+ # Set title (from enhancements or derive from model name)
73
+ title = schema_enhancements['title'] || model.name.demodulize.humanize
74
+ schema_def.title(title)
75
+
76
+ # Set description if provided
77
+ if (description = schema_enhancements['description'])
78
+ schema_def.description(description)
79
+ end
80
+
81
+ # Set additionalProperties (from enhancements or configuration default)
82
+ additional_props = if schema_enhancements.key?('additionalProperties')
83
+ schema_enhancements['additionalProperties']
84
+ else
85
+ EasyTalk.configuration.default_additional_properties
86
+ end
87
+ schema_def.additional_properties(additional_props)
88
+ end
89
+
90
+ # Add properties based on database columns
91
+ #
92
+ # @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
93
+ def add_column_properties(schema_def)
94
+ filtered_columns.each do |column|
95
+ # Get column enhancement info if it exists
96
+ column_enhancements = schema_enhancements.dig('properties', column.name.to_s) || {}
97
+
98
+ # Map the database type to Ruby type
99
+ ruby_type = COLUMN_TYPE_MAP.fetch(column.type, String)
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
+
104
+ # Build constraints hash for this column
105
+ constraints = build_column_constraints(column, column_enhancements)
106
+
107
+ # Add the property to schema definition
108
+ schema_def.property(column.name.to_sym, ruby_type, constraints)
109
+ end
110
+ end
111
+
112
+ # Build constraints hash for a database column
113
+ #
114
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The database column
115
+ # @param enhancements [Hash] Any schema enhancements for this column
116
+ # @return [Hash] The constraints hash
117
+ def build_column_constraints(column, enhancements)
118
+ constraints = {
119
+ optional: enhancements['optional'],
120
+ description: enhancements['description'],
121
+ title: enhancements['title']
122
+ }
123
+
124
+ # Add format constraint for date/time columns
125
+ if (format = FORMAT_MAP[column.type])
126
+ constraints[:format] = format
127
+ end
128
+
129
+ # Add max_length for string columns with limits
130
+ constraints[:max_length] = column.limit if column.type == :string && column.limit
131
+
132
+ # Add precision/scale for numeric columns
133
+ if column.type == :decimal && column.precision
134
+ constraints[:precision] = column.precision
135
+ constraints[:scale] = column.scale if column.scale
136
+ end
137
+
138
+ # Add default value if present and not a proc
139
+ constraints[:default] = column.default if column.default && !column.default.is_a?(Proc)
140
+
141
+ # Remove nil values
142
+ constraints.compact
143
+ end
144
+
145
+ # Add properties based on ActiveRecord associations
146
+ #
147
+ # @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
148
+ def add_association_properties(schema_def)
149
+ model.reflect_on_all_associations.each do |association|
150
+ # Skip if we can't determine the class or it's in the association exclusion list
151
+ next if association_excluded?(association)
152
+
153
+ # Get association enhancement info if it exists
154
+ assoc_enhancements = schema_enhancements.dig('properties', association.name.to_s) || {}
155
+
156
+ case association.macro
157
+ when :belongs_to, :has_one
158
+ schema_def.property(
159
+ association.name,
160
+ association.klass,
161
+ { optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
162
+ )
163
+ when :has_many, :has_and_belongs_to_many
164
+ schema_def.property(
165
+ association.name,
166
+ T::Array[association.klass],
167
+ { optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
168
+ )
169
+ end
170
+ end
171
+ end
172
+
173
+ # Add virtual properties defined in schema_enhancements
174
+ #
175
+ # @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
176
+ def add_virtual_properties(schema_def)
177
+ return unless schema_enhancements['properties']
178
+
179
+ schema_enhancements['properties'].each do |name, options|
180
+ next unless options['virtual']
181
+
182
+ # Map string type name to Ruby class
183
+ ruby_type = map_type_string_to_ruby_class(options['type'] || 'string')
184
+
185
+ # Build constraints for virtual property
186
+ constraints = {
187
+ description: options['description'],
188
+ title: options['title'],
189
+ optional: options['optional'],
190
+ format: options['format'],
191
+ default: options['default'],
192
+ min_length: options['minLength'],
193
+ max_length: options['maxLength'],
194
+ enum: options['enum']
195
+ }.compact
196
+
197
+ # Add the virtual property
198
+ schema_def.property(name.to_sym, ruby_type, constraints)
199
+ end
200
+ end
201
+
202
+ # Map a type string to a Ruby class
203
+ #
204
+ # @param type_str [String] The type string (e.g., 'string', 'integer')
205
+ # @return [Class] The corresponding Ruby class
206
+ def map_type_string_to_ruby_class(type_str)
207
+ case type_str.to_s.downcase
208
+ when 'string' then String
209
+ when 'integer' then Integer
210
+ when 'number' then Float
211
+ when 'boolean' then T::Boolean
212
+ when 'object' then Hash
213
+ when 'array' then Array
214
+ when 'date' then Date
215
+ when 'datetime' then DateTime
216
+ when 'time' then Time
217
+ else String # Default fallback
218
+ end
219
+ end
220
+
221
+ # Get all columns that should be included in the schema
222
+ #
223
+ # @return [Array<ActiveRecord::ConnectionAdapters::Column>] Filtered columns
224
+ def filtered_columns
225
+ model.columns.reject do |column|
226
+ config = EasyTalk.configuration
227
+ excluded_columns.include?(column.name.to_sym) ||
228
+ (config.exclude_primary_key && column.name == model.primary_key) ||
229
+ (config.exclude_timestamps && timestamp_column?(column.name)) ||
230
+ (config.exclude_foreign_keys && foreign_key_column?(column.name))
231
+ end
232
+ end
233
+
234
+ # Check if a column is a timestamp column
235
+ #
236
+ # @param column_name [String] The column name
237
+ # @return [Boolean] True if the column is a timestamp column
238
+ def timestamp_column?(column_name)
239
+ %w[created_at updated_at].include?(column_name)
240
+ end
241
+
242
+ # Check if a column is a foreign key column
243
+ #
244
+ # @param column_name [String] The column name
245
+ # @return [Boolean] True if the column is a foreign key column
246
+ def foreign_key_column?(column_name)
247
+ column_name.end_with?('_id')
248
+ end
249
+
250
+ # Check if an association should be excluded
251
+ #
252
+ # @param association [ActiveRecord::Reflection::AssociationReflection] The association
253
+ # @return [Boolean] True if the association should be excluded
254
+ def association_excluded?(association)
255
+ !association.klass ||
256
+ excluded_associations.include?(association.name.to_sym) ||
257
+ association.options[:polymorphic] # Skip polymorphic associations (complex to model)
258
+ end
259
+
260
+ # Get schema enhancements
261
+ #
262
+ # @return [Hash] Schema enhancements
263
+ def schema_enhancements
264
+ @schema_enhancements ||= if model.respond_to?(:schema_enhancements)
265
+ model.schema_enhancements.deep_transform_keys(&:to_s)
266
+ else
267
+ {}
268
+ end
269
+ end
270
+
271
+ # Get all excluded columns
272
+ #
273
+ # @return [Array<Symbol>] Excluded column names
274
+ def excluded_columns
275
+ @excluded_columns ||= begin
276
+ config = EasyTalk.configuration
277
+ global_exclusions = config.excluded_columns || []
278
+ model_exclusions = schema_enhancements['ignore'] || []
279
+
280
+ # Combine and convert to symbols for consistent comparison
281
+ (global_exclusions + model_exclusions).map(&:to_sym)
282
+ end
283
+ end
284
+
285
+ # Get all excluded associations
286
+ #
287
+ # @return [Array<Symbol>] Excluded association names
288
+ def excluded_associations
289
+ @excluded_associations ||= begin
290
+ model_exclusions = schema_enhancements['ignore_associations'] || []
291
+ model_exclusions.map(&:to_sym)
292
+ end
293
+ end
294
+ end
295
+ end
@@ -15,11 +15,11 @@ module EasyTalk
15
15
  optional: { type: T.nilable(T::Boolean), key: :optional }
16
16
  }.freeze
17
17
 
18
- attr_reader :name, :schema, :options
18
+ attr_reader :property_name, :schema, :options
19
19
 
20
20
  sig do
21
21
  params(
22
- name: Symbol,
22
+ property_name: Symbol,
23
23
  schema: T::Hash[Symbol, T.untyped],
24
24
  options: T::Hash[Symbol, String],
25
25
  valid_options: T::Hash[Symbol, T.untyped]
@@ -27,14 +27,14 @@ module EasyTalk
27
27
  end
28
28
  # Initializes a new instance of the BaseBuilder class.
29
29
  #
30
- # @param name [Symbol] The name of the property.
30
+ # @param property_name [Symbol] The name of the property.
31
31
  # @param schema [Hash] A hash representing a json schema object.
32
32
  # @param options [Hash] The options for the builder (default: {}).
33
33
  # @param valid_options [Hash] The acceptable options for the given property type (default: {}).
34
- def initialize(name, schema, options = {}, valid_options = {})
34
+ def initialize(property_name, schema, options = {}, valid_options = {})
35
35
  @valid_options = COMMON_OPTIONS.merge(valid_options)
36
- options.assert_valid_keys(@valid_options.keys)
37
- @name = name
36
+ EasyTalk.assert_valid_property_options(property_name, options, @valid_options.keys)
37
+ @property_name = property_name
38
38
  @schema = schema
39
39
  @options = options
40
40
  end
@@ -42,16 +42,18 @@ module EasyTalk
42
42
  # Builds the schema object based on the provided options.
43
43
  sig { returns(T::Hash[Symbol, T.untyped]) }
44
44
  def build
45
- @valid_options.each_with_object(schema) do |(key, value), obj|
46
- next if @options[key].nil?
45
+ @valid_options.each_with_object(schema) do |(constraint_name, value), obj|
46
+ next if @options[constraint_name].nil?
47
47
 
48
- # Work around for Sorbet's default inability to type check the items inside an array
48
+ # Use our centralized validation
49
+ ErrorHelper.validate_constraint_value(
50
+ property_name: property_name,
51
+ constraint_name: constraint_name,
52
+ value_type: value[:type],
53
+ value: @options[constraint_name]
54
+ )
49
55
 
50
- if value[:type].respond_to?(:recursively_valid?) && !value[:type].recursively_valid?(@options[key])
51
- raise TypeError, "Invalid type for #{key}"
52
- end
53
-
54
- obj[value[:key]] = T.let(@options[key], value[:type])
56
+ obj[value[:key]] = @options[constraint_name]
55
57
  end
56
58
  end
57
59
 
@@ -60,6 +60,18 @@ module EasyTalk
60
60
  def items
61
61
  @type.items
62
62
  end
63
+
64
+ # Builder class for AllOf composition.
65
+ class AllOfBuilder < CompositionBuilder
66
+ end
67
+
68
+ # Builder class for AnyOf composition.
69
+ class AnyOfBuilder < CompositionBuilder
70
+ end
71
+
72
+ # Builder class for OneOf composition.
73
+ class OneOfBuilder < CompositionBuilder
74
+ end
63
75
  end
64
76
  end
65
77
  end
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
2
+
1
3
  require_relative 'base_builder'
2
- require 'set'
3
4
 
4
5
  module EasyTalk
5
6
  module Builders
@@ -68,6 +69,9 @@ module EasyTalk
68
69
  # Populate the final "required" array from @required_properties
69
70
  merged[:required] = @required_properties.to_a if @required_properties.any?
70
71
 
72
+ # Add additionalProperties: false by default if not explicitly set
73
+ merged[:additional_properties] = false unless merged.key?(:additional_properties)
74
+
71
75
  # Prune empty or nil values so we don't produce stuff like "properties": {} unnecessarily
72
76
  merged.reject! { |_k, v| v.nil? || v == {} || v == [] }
73
77
 
@@ -114,6 +118,11 @@ module EasyTalk
114
118
  # Check constraints[:optional]
115
119
  return true if prop_options.dig(:constraints, :optional)
116
120
 
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
125
+
117
126
  false
118
127
  end
119
128
 
@@ -129,8 +138,10 @@ module EasyTalk
129
138
  # This indicates block-style definition => nested schema
130
139
  nested_schema_builder(prop_options)
131
140
  else
141
+ # Remove optional constraints from the property
142
+ constraints = prop_options[:constraints].except(:optional)
132
143
  # Normal property: e.g. { type: String, constraints: {...} }
133
- Property.new(prop_name, prop_options[:type], prop_options[:constraints])
144
+ Property.new(prop_name, prop_options[:type], constraints)
134
145
  end
135
146
  end
136
147
 
@@ -15,8 +15,7 @@ module EasyTalk
15
15
  max_length: { type: Integer, key: :maxLength },
16
16
  enum: { type: T::Array[String], key: :enum },
17
17
  const: { type: String, key: :const },
18
- default: { type: String, key: :default },
19
- optional: { type: T::Boolean, key: :optional }
18
+ default: { type: String, key: :default }
20
19
  }.freeze
21
20
 
22
21
  sig { params(name: Symbol, constraints: Hash).void }
@@ -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
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
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
8
+
9
+ 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
+ @default_additional_properties = false
16
+ @nilable_is_optional = false
17
+ end
18
+ end
19
+
20
+ class << self
21
+ def configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ def configure
26
+ yield(configuration)
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ class Error < StandardError; end
5
+ class ConstraintError < Error; end
6
+ class UnknownOptionError < Error; end
7
+ class InvalidPropertyNameError < Error; end
8
+ end
@@ -0,0 +1,147 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module ErrorHelper
5
+ def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
6
+ message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
7
+ "but received #{got.inspect} (#{got.class})."
8
+ raise ConstraintError, message
9
+ end
10
+
11
+ def self.raise_array_constraint_error(property_name:, constraint_name:, index:, expected:, got:)
12
+ message = "Error in property '#{property_name}': Constraint '#{constraint_name}' at index #{index} " \
13
+ "expects #{expected}, but received #{got.inspect} (#{got.class})."
14
+ raise ConstraintError, message
15
+ end
16
+
17
+ def self.raise_unknown_option_error(property_name:, option:, valid_options:)
18
+ option = option.keys.first if option.is_a?(Hash)
19
+ message = "Unknown option '#{option}' for property '#{property_name}'. " \
20
+ "Valid options are: #{valid_options.join(', ')}."
21
+ raise UnknownOptionError, message
22
+ end
23
+
24
+ def self.extract_inner_type(type_info)
25
+ # No change needed here
26
+ if type_info.respond_to?(:type) && type_info.type.respond_to?(:raw_type)
27
+ type_info.type.raw_type
28
+ # special boolean handling
29
+ elsif type_info.try(:type).try(:name) == 'T::Boolean'
30
+ T::Boolean
31
+ elsif type_info.respond_to?(:type_parameter)
32
+ type_info.type_parameter
33
+ elsif type_info.respond_to?(:raw_a) && type_info.respond_to?(:raw_b)
34
+ # Handle SimplePairUnion types
35
+ [type_info.raw_a, type_info.raw_b]
36
+ elsif type_info.respond_to?(:types)
37
+ # Handle complex union types
38
+ type_info.types.map { |t| t.respond_to?(:raw_type) ? t.raw_type : t }
39
+ else
40
+ # Fallback to something sensible
41
+ Object
42
+ end
43
+ end
44
+
45
+ def self.validate_typed_array_values(property_name:, constraint_name:, type_info:, array_value:)
46
+ # Skip validation if it's not actually an array
47
+ return unless array_value.is_a?(Array)
48
+
49
+ # Extract the inner type from the array type definition
50
+ inner_type = extract_inner_type(type_info)
51
+
52
+ # Check each element of the array
53
+ array_value.each_with_index do |element, index|
54
+ if inner_type.is_a?(Array)
55
+ # For union types, check if the element matches any of the allowed types
56
+ unless inner_type.any? { |t| element.is_a?(t) }
57
+ expected = inner_type.join(' or ')
58
+ raise_array_constraint_error(
59
+ property_name: property_name,
60
+ constraint_name: constraint_name,
61
+ index: index,
62
+ expected: expected,
63
+ got: element
64
+ )
65
+ end
66
+ else
67
+ # For single types, just check against that type
68
+ next if [true, false].include?(element)
69
+
70
+ unless element.is_a?(inner_type)
71
+ raise_array_constraint_error(
72
+ property_name: property_name,
73
+ constraint_name: constraint_name,
74
+ index: index,
75
+ expected: inner_type,
76
+ got: element
77
+ )
78
+ end
79
+ end
80
+ end
81
+ end
82
+
83
+ def self.validate_constraint_value(property_name:, constraint_name:, value_type:, value:)
84
+ return if value.nil?
85
+
86
+ if value_type.to_s.include?('Boolean')
87
+ return if value.is_a?(Array) && value.all? { |v| [true, false].include?(v) }
88
+
89
+ unless [true, false].include?(value)
90
+ raise_constraint_error(
91
+ property_name: property_name,
92
+ constraint_name: constraint_name,
93
+ expected: 'Boolean (true or false)',
94
+ got: value
95
+ )
96
+ end
97
+ return
98
+ end
99
+
100
+ # Handle simple scalar types (String, Integer, etc.)
101
+ if value_type.is_a?(Class)
102
+ unless value.is_a?(value_type)
103
+ raise_constraint_error(
104
+ property_name: property_name,
105
+ constraint_name: constraint_name,
106
+ expected: value_type,
107
+ got: value
108
+ )
109
+ end
110
+ # Handle array types specifically
111
+ elsif value_type.class.name.include?('TypedArray') ||
112
+ (value_type.respond_to?(:to_s) && value_type.to_s.include?('T::Array'))
113
+ # This is an array type, validate it
114
+ validate_typed_array_values(
115
+ property_name: property_name,
116
+ constraint_name: constraint_name,
117
+ type_info: value_type,
118
+ array_value: value
119
+ )
120
+ # Handle Sorbet type objects
121
+ elsif value_type.class.ancestors.include?(T::Types::Base)
122
+ # Extract the inner type
123
+ inner_type = extract_inner_type(value_type)
124
+
125
+ if inner_type.is_a?(Array)
126
+ # For union types, check if the value matches any of the allowed types
127
+ unless inner_type.any? { |t| value.is_a?(t) }
128
+ expected = inner_type.join(' or ')
129
+ raise_constraint_error(
130
+ property_name: property_name,
131
+ constraint_name: constraint_name,
132
+ expected: expected,
133
+ got: value
134
+ )
135
+ end
136
+ elsif !value.is_a?(inner_type)
137
+ raise_constraint_error(
138
+ property_name: property_name,
139
+ constraint_name: constraint_name,
140
+ expected: inner_type,
141
+ got: value
142
+ )
143
+ end
144
+ end
145
+ end
146
+ end
147
+ end