easy_talk 1.0.0 → 1.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fa39fe0359df9334a186807b3e67679b752806db59eb9b03829ec875c6382818
4
- data.tar.gz: 150814754a0604fc0149bf042c73d798fe042935ffde11b52c2254ac765c05e8
3
+ metadata.gz: d7f671ab545b8ab502b6bc9412855fa8a5c5fd45a55034e1c7eb6dba000539cc
4
+ data.tar.gz: 7243055ab896f4a7d73da4c6b2c9e425d71651815d1cca9cab1a23b4c5159497
5
5
  SHA512:
6
- metadata.gz: ea9d64a999260983afac690850ae5095b4e2d00583feb1a6dd4baa0a0cb377a82566dae1245e1b767e5ff79549f28e60209b43fa3d765a8b51c46ee6969425bd
7
- data.tar.gz: 20e3bea29ad389126937924f431f8729be43b172f8f868ae5fc0189d729e1d19642a9fa0e74a34322e1acafd2b74c6b2fad20bb8f081c7ea504364a2daaf3d99
6
+ metadata.gz: ba14fb1e04f2cda11bff0d72f0f8f52e27b7a04c86f182cae43cc487b5e33369abae7b9d55d5af732063ca54ffe5bfcf97bc2a14344fb329d07199cef0a1b075
7
+ data.tar.gz: 02ffbf919e5ab09eb253c055da2b1a787419dc5f0b9b6b60901176bac039cd323134482c6551a15730f1d9dd4a0268336a5bbce47803a7f348e85c2950f2c75e
data/CHANGELOG.md CHANGED
@@ -1,3 +1,35 @@
1
+ ## [1.0.2] - 2024-13-01
2
+ - Support "AdditionalProperties". see https://json-schema.org/understanding-json-schema/reference/object#additionalproperties
3
+ You can now define a schema that allows any additional properties.
4
+ ```ruby
5
+ class Company
6
+ include EasyTalk::Model
7
+
8
+ define_schema do
9
+ property :name, String
10
+ additional_properties true # or false
11
+ end
12
+ end
13
+ ```
14
+
15
+ You can then do:
16
+ ```ruby
17
+ company = Company.new
18
+ company.name = "Acme Corp" # Defined property
19
+ company.location = "New York" # Additional property
20
+ company.employee_count = 100 # Additional property
21
+ ```
22
+
23
+ company.as_json
24
+ # => {
25
+ # "name" => "Acme Corp",
26
+ # "location" => "New York",
27
+ # "employee_count" => 100
28
+ # }
29
+ ```
30
+ - Fix that we don't conflate nilable properties with optional properties.
31
+ ## [1.0.1] - 2024-09-01
32
+ - Fixed that property with custom type does not ignore the constraints hash https://github.com/sergiobayona/easy_talk/issues/17
1
33
  ## [1.0.0] - 2024-06-01
2
34
  - Use `Hash` instead of `:object` for inline object schema definition.
3
35
  example:
data/README.md CHANGED
@@ -176,6 +176,112 @@ class Payment
176
176
  end
177
177
  ```
178
178
 
179
+ ## Additional Properties
180
+
181
+ EasyTalk supports the JSON Schema `additionalProperties` keyword, allowing you to control whether instances of your model can accept properties beyond those explicitly defined in the schema.
182
+
183
+ ### Usage
184
+
185
+ Use the `additional_properties` keyword in your schema definition to specify whether additional properties are allowed:
186
+
187
+ ```ruby
188
+ class Company
189
+ include EasyTalk::Model
190
+
191
+ define_schema do
192
+ property :name, String
193
+ additional_properties true # Allow additional properties
194
+ end
195
+ end
196
+
197
+ # Additional properties are allowed
198
+ company = Company.new
199
+ company.name = "Acme Corp" # Defined property
200
+ company.location = "New York" # Additional property
201
+ company.employee_count = 100 # Additional property
202
+
203
+ company.as_json
204
+ # => {
205
+ # "name" => "Acme Corp",
206
+ # "location" => "New York",
207
+ # "employee_count" => 100
208
+ # }
209
+ ```
210
+
211
+ ### Behavior
212
+
213
+ When `additional_properties true`:
214
+ - Instances can accept properties beyond those defined in the schema
215
+ - Additional properties can be set both via the constructor and direct assignment
216
+ - Additional properties are included in JSON serialization
217
+ - Attempting to access an undefined additional property raises NoMethodError
218
+
219
+ ```ruby
220
+ # Setting via constructor
221
+ company = Company.new(
222
+ name: "Acme Corp",
223
+ location: "New York" # Additional property
224
+ )
225
+
226
+ # Setting via assignment
227
+ company.rank = 1 # Additional property
228
+
229
+ # Accessing undefined properties
230
+ company.undefined_prop # Raises NoMethodError
231
+ ```
232
+
233
+ When `additional_properties false` or not specified:
234
+ - Only properties defined in the schema are allowed
235
+ - Attempting to set or get undefined properties raises NoMethodError
236
+
237
+ ```ruby
238
+ class RestrictedCompany
239
+ include EasyTalk::Model
240
+
241
+ define_schema do
242
+ property :name, String
243
+ additional_properties false # Restrict to defined properties only
244
+ end
245
+ end
246
+
247
+ company = RestrictedCompany.new
248
+ company.name = "Acme Corp" # OK - defined property
249
+ company.location = "New York" # Raises NoMethodError
250
+ ```
251
+
252
+ ### JSON Schema
253
+
254
+ The `additional_properties` setting is reflected in the generated JSON Schema:
255
+
256
+ ```ruby
257
+ Company.json_schema
258
+ # => {
259
+ # "type" => "object",
260
+ # "properties" => {
261
+ # "name" => { "type" => "string" }
262
+ # },
263
+ # "required" => ["name"],
264
+ # "additionalProperties" => true
265
+ # }
266
+ ```
267
+
268
+ ### Best Practices
269
+
270
+ 1. **Default to Restrictive**: Unless you specifically need additional properties, it's recommended to leave `additional_properties` as false (the default) to maintain schema integrity.
271
+
272
+ 2. **Documentation**: If you enable additional properties, document the expected additional property types and their purpose.
273
+
274
+ 3. **Validation**: Consider implementing custom validation for additional properties if they need to conform to specific patterns or types.
275
+
276
+ 4. **Error Handling**: When working with instances that allow additional properties, use `respond_to?` or `try` to handle potentially undefined properties safely:
277
+
278
+ ```ruby
279
+ # Safe property access
280
+ value = company.try(:optional_property)
281
+ # or
282
+ value = company.optional_property if company.respond_to?(:optional_property)
283
+ ```
284
+
179
285
  ## Type Checking and Schema Constraints
180
286
 
181
287
  EasyTalk uses a combination of standard Ruby types (`String`, `Integer`), Sorbet types (`T::Boolean`, `T::Array[String]`, etc.), and custom Sorbet-style types (`T::AnyOf[]`, `T::OneOf[]`) to perform basic type checking. For example:
@@ -8,7 +8,7 @@ module EasyTalk
8
8
  # into a validated JSON Schema hash. It:
9
9
  #
10
10
  # 1) Recursively processes the schema’s :properties,
11
- # 2) Determines which properties are required (unless nilable or optional),
11
+ # 2) Determines which properties are required (unless optional),
12
12
  # 3) Handles sub-schema composition (allOf, anyOf, oneOf, not),
13
13
  # 4) Produces the final object-level schema hash.
14
14
  #
@@ -108,15 +108,9 @@ module EasyTalk
108
108
  end
109
109
 
110
110
  ##
111
- # Returns true if the property is declared optional or is T.nilable(...).
111
+ # Returns true if the property is declared optional.
112
112
  #
113
113
  def property_optional?(prop_options)
114
- # For convenience, treat :type as an object
115
- type_obj = prop_options[:type]
116
-
117
- # Check Sorbet's nilable (like T.nilable(String))
118
- return true if type_obj.respond_to?(:nilable?) && type_obj.nilable?
119
-
120
114
  # Check constraints[:optional]
121
115
  return true if prop_options.dig(:constraints, :optional)
122
116
 
@@ -136,7 +130,6 @@ module EasyTalk
136
130
  nested_schema_builder(prop_options)
137
131
  else
138
132
  # Normal property: e.g. { type: String, constraints: {...} }
139
- handle_nilable_type(prop_options)
140
133
  Property.new(prop_name, prop_options[:type], prop_options[:constraints])
141
134
  end
142
135
  end
@@ -147,24 +140,9 @@ module EasyTalk
147
140
  def nested_schema_builder(prop_options)
148
141
  child_schema_def = prop_options[:properties]
149
142
  # If user used T.nilable(...) with a block, unwrap the nilable
150
- handle_nilable_type(prop_options)
151
143
  ObjectBuilder.new(child_schema_def).build
152
144
  end
153
145
 
154
- ##
155
- # If the type is T.nilable(SomeType), unwrap it so we produce the correct schema.
156
- # This logic is borrowed from the old #handle_option_type method.
157
- #
158
- def handle_nilable_type(prop_options)
159
- type_obj = prop_options[:type]
160
- return unless type_obj.respond_to?(:nilable?) && type_obj.nilable?
161
-
162
- # If the underlying raw_type isn't T::Types::TypedArray, then we unwrap it
163
- return unless type_obj.unwrap_nilable.class != T::Types::TypedArray
164
-
165
- prop_options[:type] = type_obj.unwrap_nilable.raw_type
166
- end
167
-
168
146
  ##
169
147
  # Process top-level composition keywords (e.g. allOf, anyOf, oneOf),
170
148
  # converting them to definitions + references if appropriate.
@@ -38,6 +38,50 @@ module EasyTalk
38
38
  base.include ActiveModel::Validations
39
39
  base.extend ActiveModel::Callbacks
40
40
  base.extend(ClassMethods)
41
+ base.include(InstanceMethods)
42
+ end
43
+
44
+ module InstanceMethods
45
+ def initialize(attributes = {})
46
+ @additional_properties = {}
47
+ super
48
+ end
49
+
50
+ def method_missing(method_name, *args)
51
+ method_string = method_name.to_s
52
+ if method_string.end_with?('=')
53
+ property_name = method_string.chomp('=')
54
+ if self.class.additional_properties_allowed?
55
+ @additional_properties[property_name] = args.first
56
+ else
57
+ super
58
+ end
59
+ elsif self.class.additional_properties_allowed? && @additional_properties.key?(method_string)
60
+ @additional_properties[method_string]
61
+ else
62
+ super
63
+ end
64
+ end
65
+
66
+ def respond_to_missing?(method_name, include_private = false)
67
+ method_string = method_name.to_s
68
+ method_string.end_with?('=') ? method_string.chomp('=') : method_string
69
+ self.class.additional_properties_allowed? || super
70
+ end
71
+
72
+ # Add to_hash method to convert defined properties to hash
73
+ def to_hash
74
+ return {} unless self.class.properties
75
+
76
+ self.class.properties.each_with_object({}) do |prop, hash|
77
+ hash[prop.to_s] = send(prop)
78
+ end
79
+ end
80
+
81
+ # Override as_json to include both defined and additional properties
82
+ def as_json(_options = {})
83
+ to_hash.merge(@additional_properties)
84
+ end
41
85
  end
42
86
 
43
87
  # Module containing class-level methods for defining and accessing the schema of a model.
@@ -92,6 +136,10 @@ module EasyTalk
92
136
  @schema_definition ||= {}
93
137
  end
94
138
 
139
+ def additional_properties_allowed?
140
+ @schema_definition&.schema&.fetch(:additional_properties, false)
141
+ end
142
+
95
143
  private
96
144
 
97
145
  # Builds the schema using the provided schema definition.
@@ -76,15 +76,17 @@ module EasyTalk
76
76
  #
77
77
  # @return [Object] The built property.
78
78
  def build
79
- # return type.respond_to?(:schema) ? type.schema : 'object' unless builder
80
-
81
- # args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
82
- # builder.new(*args).build
83
- if builder
79
+ if nilable_type?
80
+ build_nilable_schema
81
+ elsif builder
84
82
  args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
85
83
  builder.new(*args).build
84
+ elsif type.respond_to?(:schema)
85
+ # merge the top-level constraints from *this* property
86
+ # e.g. :title, :description, :default, etc
87
+ type.schema.merge!(constraints)
86
88
  else
87
- type.respond_to?(:schema) ? type.schema : 'object'
89
+ 'object'
88
90
  end
89
91
  end
90
92
 
@@ -105,5 +107,27 @@ module EasyTalk
105
107
  def builder
106
108
  @builder ||= TYPE_TO_BUILDER[type.class.name.to_s] || TYPE_TO_BUILDER[type.name.to_s]
107
109
  end
110
+
111
+ private
112
+
113
+ def nilable_type?
114
+ return unless type.respond_to?(:types)
115
+ return unless type.types.all? { |t| t.respond_to?(:raw_type) }
116
+
117
+ type.types.any? { |t| t.raw_type == NilClass }
118
+ end
119
+
120
+ def build_nilable_schema
121
+ # Extract the non-nil type from the Union
122
+ actual_type = type.types.find { |t| t != NilClass }
123
+
124
+ # Create a property with the actual type
125
+ non_nil_schema = Property.new(name, actual_type, constraints).build
126
+
127
+ # Merge the types into an array
128
+ non_nil_schema.merge(
129
+ type: [non_nil_schema[:type], 'null']
130
+ )
131
+ end
108
132
  end
109
133
  end
@@ -4,13 +4,13 @@ require_relative 'keywords'
4
4
 
5
5
  module EasyTalk
6
6
  class InvalidPropertyNameError < StandardError; end
7
+
7
8
  #
8
9
  #= EasyTalk \SchemaDefinition
9
10
  # SchemaDefinition provides the methods for defining a schema within the define_schema block.
10
11
  # The @schema is a hash that contains the unvalidated schema definition for the model.
11
12
  # A SchemaDefinition instanace is the passed to the Builder.build_schema method to validate and compile the schema.
12
13
  class SchemaDefinition
13
-
14
14
  extend T::Sig
15
15
  extend T::AnyOf
16
16
  extend T::OneOf
@@ -56,9 +56,10 @@ module EasyTalk
56
56
  end
57
57
 
58
58
  def validate_property_name(name)
59
- unless name.to_s.match?(/^[A-Za-z_][A-Za-z0-9_]*$/)
60
- raise InvalidPropertyNameError, "Invalid property name '#{name}'. Must start with letter/underscore and contain only letters, numbers, underscores"
61
- end
59
+ return if name.to_s.match?(/^[A-Za-z_][A-Za-z0-9_]*$/)
60
+
61
+ raise InvalidPropertyNameError,
62
+ "Invalid property name '#{name}'. Must start with letter/underscore and contain only letters, numbers, underscores"
62
63
  end
63
64
 
64
65
  def optional?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module EasyTalk
4
- VERSION = '1.0.0'
4
+ VERSION = '1.0.2'
5
5
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: easy_talk
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sergio Bayona
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-09 00:00:00.000000000 Z
10
+ date: 2025-01-13 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: activemodel