easy_talk 1.0.1 → 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 +4 -4
- data/CHANGELOG.md +30 -0
- data/README.md +106 -0
- data/lib/easy_talk/builders/object_builder.rb +2 -24
- data/lib/easy_talk/model.rb +48 -0
- data/lib/easy_talk/property.rb +25 -1
- data/lib/easy_talk/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: d7f671ab545b8ab502b6bc9412855fa8a5c5fd45a55034e1c7eb6dba000539cc
|
4
|
+
data.tar.gz: 7243055ab896f4a7d73da4c6b2c9e425d71651815d1cca9cab1a23b4c5159497
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ba14fb1e04f2cda11bff0d72f0f8f52e27b7a04c86f182cae43cc487b5e33369abae7b9d55d5af732063ca54ffe5bfcf97bc2a14344fb329d07199cef0a1b075
|
7
|
+
data.tar.gz: 02ffbf919e5ab09eb253c055da2b1a787419dc5f0b9b6b60901176bac039cd323134482c6551a15730f1d9dd4a0268336a5bbce47803a7f348e85c2950f2c75e
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,33 @@
|
|
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.
|
1
31
|
## [1.0.1] - 2024-09-01
|
2
32
|
- Fixed that property with custom type does not ignore the constraints hash https://github.com/sergiobayona/easy_talk/issues/17
|
3
33
|
## [1.0.0] - 2024-06-01
|
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
|
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
|
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.
|
data/lib/easy_talk/model.rb
CHANGED
@@ -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.
|
data/lib/easy_talk/property.rb
CHANGED
@@ -76,7 +76,9 @@ module EasyTalk
|
|
76
76
|
#
|
77
77
|
# @return [Object] The built property.
|
78
78
|
def build
|
79
|
-
if
|
79
|
+
if nilable_type?
|
80
|
+
build_nilable_schema
|
81
|
+
elsif builder
|
80
82
|
args = builder.collection_type? ? [name, type, constraints] : [name, constraints]
|
81
83
|
builder.new(*args).build
|
82
84
|
elsif type.respond_to?(:schema)
|
@@ -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
|
data/lib/easy_talk/version.rb
CHANGED
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.
|
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-
|
10
|
+
date: 2025-01-13 00:00:00.000000000 Z
|
11
11
|
dependencies:
|
12
12
|
- !ruby/object:Gem::Dependency
|
13
13
|
name: activemodel
|