easy_talk 1.0.2 → 1.0.3

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,292 @@
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
+ # Build constraints hash for this column
102
+ constraints = build_column_constraints(column, column_enhancements)
103
+
104
+ # Add the property to schema definition
105
+ schema_def.property(column.name.to_sym, ruby_type, constraints)
106
+ end
107
+ end
108
+
109
+ # Build constraints hash for a database column
110
+ #
111
+ # @param column [ActiveRecord::ConnectionAdapters::Column] The database column
112
+ # @param enhancements [Hash] Any schema enhancements for this column
113
+ # @return [Hash] The constraints hash
114
+ def build_column_constraints(column, enhancements)
115
+ constraints = {
116
+ optional: enhancements['optional'],
117
+ description: enhancements['description'],
118
+ title: enhancements['title']
119
+ }
120
+
121
+ # Add format constraint for date/time columns
122
+ if (format = FORMAT_MAP[column.type])
123
+ constraints[:format] = format
124
+ end
125
+
126
+ # Add max_length for string columns with limits
127
+ constraints[:max_length] = column.limit if column.type == :string && column.limit
128
+
129
+ # Add precision/scale for numeric columns
130
+ if column.type == :decimal && column.precision
131
+ constraints[:precision] = column.precision
132
+ constraints[:scale] = column.scale if column.scale
133
+ end
134
+
135
+ # Add default value if present and not a proc
136
+ constraints[:default] = column.default if column.default && !column.default.is_a?(Proc)
137
+
138
+ # Remove nil values
139
+ constraints.compact
140
+ end
141
+
142
+ # Add properties based on ActiveRecord associations
143
+ #
144
+ # @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
145
+ def add_association_properties(schema_def)
146
+ model.reflect_on_all_associations.each do |association|
147
+ # Skip if we can't determine the class or it's in the association exclusion list
148
+ next if association_excluded?(association)
149
+
150
+ # Get association enhancement info if it exists
151
+ assoc_enhancements = schema_enhancements.dig('properties', association.name.to_s) || {}
152
+
153
+ case association.macro
154
+ when :belongs_to, :has_one
155
+ schema_def.property(
156
+ association.name,
157
+ association.klass,
158
+ { optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
159
+ )
160
+ when :has_many, :has_and_belongs_to_many
161
+ schema_def.property(
162
+ association.name,
163
+ T::Array[association.klass],
164
+ { optional: assoc_enhancements['optional'], description: assoc_enhancements['description'] }.compact
165
+ )
166
+ end
167
+ end
168
+ end
169
+
170
+ # Add virtual properties defined in schema_enhancements
171
+ #
172
+ # @param schema_def [EasyTalk::SchemaDefinition] The schema definition to modify
173
+ def add_virtual_properties(schema_def)
174
+ return unless schema_enhancements['properties']
175
+
176
+ schema_enhancements['properties'].each do |name, options|
177
+ next unless options['virtual']
178
+
179
+ # Map string type name to Ruby class
180
+ ruby_type = map_type_string_to_ruby_class(options['type'] || 'string')
181
+
182
+ # Build constraints for virtual property
183
+ constraints = {
184
+ description: options['description'],
185
+ title: options['title'],
186
+ optional: options['optional'],
187
+ format: options['format'],
188
+ default: options['default'],
189
+ min_length: options['minLength'],
190
+ max_length: options['maxLength'],
191
+ enum: options['enum']
192
+ }.compact
193
+
194
+ # Add the virtual property
195
+ schema_def.property(name.to_sym, ruby_type, constraints)
196
+ end
197
+ end
198
+
199
+ # Map a type string to a Ruby class
200
+ #
201
+ # @param type_str [String] The type string (e.g., 'string', 'integer')
202
+ # @return [Class] The corresponding Ruby class
203
+ def map_type_string_to_ruby_class(type_str)
204
+ case type_str.to_s.downcase
205
+ when 'string' then String
206
+ when 'integer' then Integer
207
+ when 'number' then Float
208
+ when 'boolean' then T::Boolean
209
+ when 'object' then Hash
210
+ when 'array' then Array
211
+ when 'date' then Date
212
+ when 'datetime' then DateTime
213
+ when 'time' then Time
214
+ else String # Default fallback
215
+ end
216
+ end
217
+
218
+ # Get all columns that should be included in the schema
219
+ #
220
+ # @return [Array<ActiveRecord::ConnectionAdapters::Column>] Filtered columns
221
+ def filtered_columns
222
+ model.columns.reject do |column|
223
+ config = EasyTalk.configuration
224
+ excluded_columns.include?(column.name.to_sym) ||
225
+ (config.exclude_primary_key && column.name == model.primary_key) ||
226
+ (config.exclude_timestamps && timestamp_column?(column.name)) ||
227
+ (config.exclude_foreign_keys && foreign_key_column?(column.name))
228
+ end
229
+ end
230
+
231
+ # Check if a column is a timestamp column
232
+ #
233
+ # @param column_name [String] The column name
234
+ # @return [Boolean] True if the column is a timestamp column
235
+ def timestamp_column?(column_name)
236
+ %w[created_at updated_at].include?(column_name)
237
+ end
238
+
239
+ # Check if a column is a foreign key column
240
+ #
241
+ # @param column_name [String] The column name
242
+ # @return [Boolean] True if the column is a foreign key column
243
+ def foreign_key_column?(column_name)
244
+ column_name.end_with?('_id')
245
+ end
246
+
247
+ # Check if an association should be excluded
248
+ #
249
+ # @param association [ActiveRecord::Reflection::AssociationReflection] The association
250
+ # @return [Boolean] True if the association should be excluded
251
+ def association_excluded?(association)
252
+ !association.klass ||
253
+ excluded_associations.include?(association.name.to_sym) ||
254
+ association.options[:polymorphic] # Skip polymorphic associations (complex to model)
255
+ end
256
+
257
+ # Get schema enhancements
258
+ #
259
+ # @return [Hash] Schema enhancements
260
+ def schema_enhancements
261
+ @schema_enhancements ||= if model.respond_to?(:schema_enhancements)
262
+ model.schema_enhancements.deep_transform_keys(&:to_s)
263
+ else
264
+ {}
265
+ end
266
+ end
267
+
268
+ # Get all excluded columns
269
+ #
270
+ # @return [Array<Symbol>] Excluded column names
271
+ def excluded_columns
272
+ @excluded_columns ||= begin
273
+ config = EasyTalk.configuration
274
+ global_exclusions = config.excluded_columns || []
275
+ model_exclusions = schema_enhancements['ignore'] || []
276
+
277
+ # Combine and convert to symbols for consistent comparison
278
+ (global_exclusions + model_exclusions).map(&:to_sym)
279
+ end
280
+ end
281
+
282
+ # Get all excluded associations
283
+ #
284
+ # @return [Array<Symbol>] Excluded association names
285
+ def excluded_associations
286
+ @excluded_associations ||= begin
287
+ model_exclusions = schema_enhancements['ignore_associations'] || []
288
+ model_exclusions.map(&:to_sym)
289
+ end
290
+ end
291
+ end
292
+ 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
 
@@ -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
 
@@ -129,8 +133,10 @@ module EasyTalk
129
133
  # This indicates block-style definition => nested schema
130
134
  nested_schema_builder(prop_options)
131
135
  else
136
+ # Remove optional constraints from the property
137
+ constraints = prop_options[:constraints].except(:optional)
132
138
  # Normal property: e.g. { type: String, constraints: {...} }
133
- Property.new(prop_name, prop_options[:type], prop_options[:constraints])
139
+ Property.new(prop_name, prop_options[:type], constraints)
134
140
  end
135
141
  end
136
142
 
@@ -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,27 @@
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
+
8
+ def initialize
9
+ @exclude_foreign_keys = true
10
+ @exclude_associations = true
11
+ @excluded_columns = []
12
+ @exclude_primary_key = true # New option, defaulting to true
13
+ @exclude_timestamps = true # New option, defaulting to true
14
+ @default_additional_properties = false
15
+ end
16
+ end
17
+
18
+ class << self
19
+ def configuration
20
+ @configuration ||= Configuration.new
21
+ end
22
+
23
+ def configure
24
+ yield(configuration)
25
+ end
26
+ end
27
+ 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
@@ -9,6 +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
13
 
13
14
  module EasyTalk
14
15
  # The `Model` module is a mixin that provides functionality for defining and accessing the schema of a model.
@@ -39,6 +40,11 @@ module EasyTalk
39
40
  base.extend ActiveModel::Callbacks
40
41
  base.extend(ClassMethods)
41
42
  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)
42
48
  end
43
49
 
44
50
  module InstanceMethods
@@ -90,7 +96,16 @@ module EasyTalk
90
96
  #
91
97
  # @return [Schema] The schema for the model.
92
98
  def schema
93
- @schema ||= build_schema(schema_definition)
99
+ @schema ||= if defined?(@schema_definition) && @schema_definition
100
+ # Schema defined explicitly via define_schema
101
+ 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
+ else
106
+ # Default case - empty schema
107
+ {}
108
+ end
94
109
  end
95
110
 
96
111
  # Returns the reference template for the model.
@@ -140,9 +155,8 @@ module EasyTalk
140
155
  @schema_definition&.schema&.fetch(:additional_properties, false)
141
156
  end
142
157
 
143
- private
144
-
145
158
  # Builds the schema using the provided schema definition.
159
+ # This is the convergence point for all schema generation.
146
160
  #
147
161
  # @param schema_definition [SchemaDefinition] The schema definition.
148
162
  # @return [Schema] The validated schema.
@@ -150,5 +164,34 @@ module EasyTalk
150
164
  Builders::ObjectBuilder.new(schema_definition).build
151
165
  end
152
166
  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
153
196
  end
154
197
  end
@@ -3,8 +3,6 @@
3
3
  require_relative 'keywords'
4
4
 
5
5
  module EasyTalk
6
- class InvalidPropertyNameError < StandardError; end
7
-
8
6
  #
9
7
  #= EasyTalk \SchemaDefinition
10
8
  # SchemaDefinition provides the methods for defining a schema within the define_schema block.
@@ -20,6 +18,7 @@ module EasyTalk
20
18
 
21
19
  def initialize(name, schema = {})
22
20
  @schema = schema
21
+ @schema[:additional_properties] = false unless schema.key?(:additional_properties)
23
22
  @name = name
24
23
  end
25
24
 
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EasyTalk
4
- VERSION = '1.0.2'
4
+ VERSION = '1.0.3'
5
5
  end