easy_talk 3.1.0 → 3.3.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.
- checksums.yaml +4 -4
- data/.rubocop.yml +15 -39
- data/.yardopts +13 -0
- data/CHANGELOG.md +164 -0
- data/README.md +442 -1529
- data/Rakefile +27 -0
- data/docs/.gitignore +1 -0
- data/docs/about.markdown +28 -8
- data/docs/getting-started.markdown +102 -0
- data/docs/index.markdown +51 -4
- data/docs/json_schema_compliance.md +169 -0
- data/docs/nested-models.markdown +216 -0
- data/docs/primitive-schema-rfc.md +894 -0
- data/docs/property-types.markdown +212 -0
- data/docs/schema-definition.markdown +180 -0
- data/lib/easy_talk/builders/base_builder.rb +6 -3
- data/lib/easy_talk/builders/boolean_builder.rb +2 -1
- data/lib/easy_talk/builders/collection_helpers.rb +4 -0
- data/lib/easy_talk/builders/composition_builder.rb +16 -13
- data/lib/easy_talk/builders/integer_builder.rb +2 -1
- data/lib/easy_talk/builders/null_builder.rb +4 -1
- data/lib/easy_talk/builders/number_builder.rb +4 -1
- data/lib/easy_talk/builders/object_builder.rb +109 -33
- data/lib/easy_talk/builders/registry.rb +182 -0
- data/lib/easy_talk/builders/string_builder.rb +3 -1
- data/lib/easy_talk/builders/temporal_builder.rb +7 -0
- data/lib/easy_talk/builders/tuple_builder.rb +89 -0
- data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
- data/lib/easy_talk/builders/union_builder.rb +5 -1
- data/lib/easy_talk/configuration.rb +47 -2
- data/lib/easy_talk/error_formatter/base.rb +100 -0
- data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
- data/lib/easy_talk/error_formatter/flat.rb +38 -0
- data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
- data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
- data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
- data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
- data/lib/easy_talk/error_formatter.rb +143 -0
- data/lib/easy_talk/errors.rb +3 -0
- data/lib/easy_talk/errors_helper.rb +66 -34
- data/lib/easy_talk/json_schema_equality.rb +46 -0
- data/lib/easy_talk/keywords.rb +0 -1
- data/lib/easy_talk/model.rb +148 -89
- data/lib/easy_talk/model_helper.rb +17 -0
- data/lib/easy_talk/naming_strategies.rb +24 -0
- data/lib/easy_talk/property.rb +23 -94
- data/lib/easy_talk/ref_helper.rb +33 -0
- data/lib/easy_talk/schema.rb +199 -0
- data/lib/easy_talk/schema_definition.rb +57 -5
- data/lib/easy_talk/schema_methods.rb +111 -0
- data/lib/easy_talk/sorbet_extension.rb +1 -0
- data/lib/easy_talk/tools/function_builder.rb +1 -1
- data/lib/easy_talk/type_introspection.rb +222 -0
- data/lib/easy_talk/types/base_composer.rb +2 -1
- data/lib/easy_talk/types/composer.rb +4 -0
- data/lib/easy_talk/types/tuple.rb +77 -0
- data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
- data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
- data/lib/easy_talk/validation_adapters/base.rb +156 -0
- data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
- data/lib/easy_talk/validation_adapters/registry.rb +87 -0
- data/lib/easy_talk/validation_builder.rb +29 -309
- data/lib/easy_talk/version.rb +1 -1
- data/lib/easy_talk.rb +42 -0
- metadata +38 -7
- data/docs/404.html +0 -25
- data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
- data/easy_talk.gemspec +0 -39
|
@@ -0,0 +1,894 @@
|
|
|
1
|
+
# RFC: Primitive Schema Feature with Convention-Based Type Inference
|
|
2
|
+
|
|
3
|
+
**Status**: Proposed
|
|
4
|
+
**Author**: Claude Code
|
|
5
|
+
**Created**: 2025-12-30
|
|
6
|
+
**Updated**: 2025-12-30
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Abstract
|
|
11
|
+
|
|
12
|
+
This document proposes adding three interrelated features to EasyTalk:
|
|
13
|
+
|
|
14
|
+
1. **Primitive Type Classes** — Reusable primitive schema definitions via `EasyTalk::Primitive`
|
|
15
|
+
2. **Convention-Based Type Inference** — Auto-infer types from property names (conservative defaults)
|
|
16
|
+
3. **Symbol-Based Type Syntax** — Alternative to Ruby constants for type declarations
|
|
17
|
+
|
|
18
|
+
These features address user feedback about API verbosity and enable more expressive, DRY schema definitions while maintaining full backward compatibility.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Motivation
|
|
23
|
+
|
|
24
|
+
### Problem Statement
|
|
25
|
+
|
|
26
|
+
**1. API Verbosity**
|
|
27
|
+
|
|
28
|
+
Users have expressed that the current API requires explicit type declarations that can feel verbose:
|
|
29
|
+
|
|
30
|
+
```ruby
|
|
31
|
+
# Current API
|
|
32
|
+
property :email, String, format: 'email'
|
|
33
|
+
property :phone, String, pattern: '^\+?[1-9]\d{1,14}$'
|
|
34
|
+
property :age, Integer, minimum: 0
|
|
35
|
+
property :is_active, T::Boolean
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Common patterns are repeated across models, and property names often imply their types (`:email` is almost always an email-formatted string).
|
|
39
|
+
|
|
40
|
+
**2. JSON Schema Compliance Gap**
|
|
41
|
+
|
|
42
|
+
EasyTalk cannot generate or validate root-level primitive schemas. The `EasyTalk::Model` mixin always produces object schemas (`{ "type": "object", ... }`). This means:
|
|
43
|
+
|
|
44
|
+
- The [JSON Schema Test Suite](https://github.com/json-schema-org/JSON-Schema-Test-Suite) tests for root integers, strings, booleans, etc. must be skipped (see [json_schema_compliance.md](json_schema_compliance.md))
|
|
45
|
+
- Users cannot define standalone primitive schemas for LLM function calls or API validation
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
# NOT POSSIBLE TODAY:
|
|
49
|
+
# Generate { "type": "string", "format": "email" } as a root schema
|
|
50
|
+
# Validate a raw string value against that schema
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Goals
|
|
54
|
+
|
|
55
|
+
1. **Reduce boilerplate** for common patterns
|
|
56
|
+
2. **Enable reusable type definitions** across models
|
|
57
|
+
3. **Provide intuitive defaults** based on naming conventions
|
|
58
|
+
4. **Enable JSON Schema compliance** for root-level primitive schemas
|
|
59
|
+
5. **Maintain backward compatibility** — existing code must continue to work unchanged
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Detailed Design
|
|
64
|
+
|
|
65
|
+
### Feature 1: Primitive Type Classes
|
|
66
|
+
|
|
67
|
+
#### Overview
|
|
68
|
+
|
|
69
|
+
Create reusable primitive schema definitions that encapsulate type + constraints.
|
|
70
|
+
|
|
71
|
+
#### Syntax
|
|
72
|
+
|
|
73
|
+
```ruby
|
|
74
|
+
class Email < EasyTalk::Primitive(:string, format: 'email')
|
|
75
|
+
class PhoneNumber < EasyTalk::Primitive(:string, pattern: '^\+?[1-9]\d{1,14}$')
|
|
76
|
+
class PositiveInteger < EasyTalk::Primitive(:integer, minimum: 0)
|
|
77
|
+
class Percentage < EasyTalk::Primitive(:number, minimum: 0, maximum: 100)
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
This syntax passes constraints directly as keyword arguments, leveraging the existing builder `VALID_OPTIONS` for validation. No separate DSL is needed.
|
|
81
|
+
|
|
82
|
+
#### Usage in Models
|
|
83
|
+
|
|
84
|
+
```ruby
|
|
85
|
+
class User
|
|
86
|
+
include EasyTalk::Model
|
|
87
|
+
|
|
88
|
+
define_schema do
|
|
89
|
+
property :email, Email
|
|
90
|
+
property :phone, PhoneNumber
|
|
91
|
+
property :age, PositiveInteger
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
#### Generated Schema
|
|
97
|
+
|
|
98
|
+
```json
|
|
99
|
+
{
|
|
100
|
+
"type": "object",
|
|
101
|
+
"properties": {
|
|
102
|
+
"email": { "type": "string", "format": "email" },
|
|
103
|
+
"phone": { "type": "string", "pattern": "^\\+?[1-9]\\d{1,14}$" },
|
|
104
|
+
"age": { "type": "integer", "minimum": 0 }
|
|
105
|
+
},
|
|
106
|
+
"required": ["email", "phone", "age"]
|
|
107
|
+
}
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
#### Implementation
|
|
111
|
+
|
|
112
|
+
**New File**: `lib/easy_talk/primitive.rb`
|
|
113
|
+
|
|
114
|
+
```ruby
|
|
115
|
+
module EasyTalk
|
|
116
|
+
class Primitive
|
|
117
|
+
class << self
|
|
118
|
+
# Factory method for creating primitive type classes
|
|
119
|
+
# Usage: class Email < EasyTalk::Primitive(:string, format: 'email')
|
|
120
|
+
#
|
|
121
|
+
# Constraints are passed as keyword arguments and validated by the
|
|
122
|
+
# underlying builder's VALID_OPTIONS - no separate DSL needed.
|
|
123
|
+
def call(type_name, **constraints)
|
|
124
|
+
Class.new(self) do
|
|
125
|
+
@base_type = type_name
|
|
126
|
+
@constraints = constraints.freeze
|
|
127
|
+
|
|
128
|
+
class << self
|
|
129
|
+
attr_reader :base_type, :constraints
|
|
130
|
+
|
|
131
|
+
# IMPORTANT: Must be named `schema` (not `json_schema`) to match
|
|
132
|
+
# Property#build expectation at lib/easy_talk/property.rb:111
|
|
133
|
+
# Property checks: type.respond_to?(:schema) and calls type.schema
|
|
134
|
+
def schema
|
|
135
|
+
@schema ||= build_schema
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Public: Returns the underlying Ruby type (String, Integer, etc.)
|
|
139
|
+
# Used by ActiveModelAdapter to apply correct validations
|
|
140
|
+
def ruby_type
|
|
141
|
+
EasyTalk::TypeResolver.resolve(@base_type)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def build_schema
|
|
147
|
+
builder_class = EasyTalk::Builders::Registry.fetch(ruby_type)
|
|
148
|
+
# Builder validates constraints via its VALID_OPTIONS
|
|
149
|
+
builder = builder_class.new(:value, **@constraints)
|
|
150
|
+
builder.build
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
alias_method :[], :call
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
**Key Design Decision**: No `ConstraintCollector` DSL is needed. The existing builders already define `VALID_OPTIONS` that enumerate valid constraints with type checking. By passing constraints as keyword arguments directly to `EasyTalk::Primitive(:type, **constraints)`, we:
|
|
162
|
+
|
|
163
|
+
1. **Eliminate duplication** — builders remain the single source of truth for constraint validation
|
|
164
|
+
2. **Get automatic validation** — `BaseBuilder#build` validates constraint types via `ErrorHelper.validate_constraint_value`
|
|
165
|
+
3. **Simplify the API** — one syntax (`key: value`) instead of learning DSL methods
|
|
166
|
+
|
|
167
|
+
#### ActiveModel Validation Integration (CRITICAL)
|
|
168
|
+
|
|
169
|
+
**Problem**: The `ActiveModelAdapter#apply_type_validations` method checks for `String`, `Integer`, `Float`, etc., but has no handling for `Primitive` subclasses. Without a fix:
|
|
170
|
+
- ✅ JSON Schema generation works correctly
|
|
171
|
+
- ❌ ActiveModel validations are NOT generated
|
|
172
|
+
|
|
173
|
+
**Solution**: Unwrap Primitive types in the adapter.
|
|
174
|
+
|
|
175
|
+
**Modified File**: `lib/easy_talk/validation_adapters/active_model_adapter.rb`
|
|
176
|
+
|
|
177
|
+
```ruby
|
|
178
|
+
def apply_type_validations(type)
|
|
179
|
+
# Handle Primitive subclasses - unwrap to base type and merge constraints
|
|
180
|
+
if type.respond_to?(:ancestors) && type.ancestors.include?(EasyTalk::Primitive)
|
|
181
|
+
# Merge Primitive's constraints with property constraints (property wins on conflict)
|
|
182
|
+
@constraints = type.constraints.merge(@constraints)
|
|
183
|
+
# Recurse with the underlying Ruby type
|
|
184
|
+
return apply_type_validations(type.ruby_type)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# ... existing type checks for String, Integer, etc. ...
|
|
188
|
+
end
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
#### Property Integration
|
|
192
|
+
|
|
193
|
+
**No changes needed to `lib/easy_talk/property.rb`**. The existing code already handles types with `.schema`:
|
|
194
|
+
|
|
195
|
+
```ruby
|
|
196
|
+
# Line 111-114 in property.rb (existing code)
|
|
197
|
+
elsif type.respond_to?(:schema)
|
|
198
|
+
type.schema.merge!(constraints)
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Since Primitive implements `.schema`, Property will:
|
|
202
|
+
1. Detect `Email.respond_to?(:schema)` → true
|
|
203
|
+
2. Call `Email.schema` → `{ type: 'string', format: 'email' }`
|
|
204
|
+
3. Merge property-level constraints
|
|
205
|
+
|
|
206
|
+
#### Validation: Two Contexts
|
|
207
|
+
|
|
208
|
+
Primitives can be validated in two different contexts:
|
|
209
|
+
|
|
210
|
+
**Context 1: Wrapped in a Model (property validation)**
|
|
211
|
+
|
|
212
|
+
When a Primitive is used as a property type within an `EasyTalk::Model`, validation happens at the Model level via ActiveModel:
|
|
213
|
+
|
|
214
|
+
```ruby
|
|
215
|
+
class Email < EasyTalk::Primitive(:string, format: 'email')
|
|
216
|
+
|
|
217
|
+
class User
|
|
218
|
+
include EasyTalk::Model
|
|
219
|
+
define_schema do
|
|
220
|
+
property :email, Email
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
user = User.new(email: "invalid")
|
|
225
|
+
user.valid? # => false (ActiveModel validation)
|
|
226
|
+
user.errors[:email] # => ["must be a valid email address"]
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The `ActiveModelAdapter` unwraps the Primitive to its base type (`String`) and merges its constraints (`format: 'email'`), then applies standard ActiveModel validations. **No special Primitive validation logic runs** — it's all standard Model validation.
|
|
230
|
+
|
|
231
|
+
**Context 2: Standalone (direct validation)**
|
|
232
|
+
|
|
233
|
+
For root-level primitive validation (e.g., JSON Schema compliance tests), Primitives provide class-level validation:
|
|
234
|
+
|
|
235
|
+
```ruby
|
|
236
|
+
class Email < EasyTalk::Primitive(:string, format: 'email')
|
|
237
|
+
|
|
238
|
+
# Class-level validation for raw values
|
|
239
|
+
Email.valid?("test@example.com") # => true
|
|
240
|
+
Email.valid?("invalid") # => false
|
|
241
|
+
Email.validate("invalid") # => ["must be a valid email address"]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
This enables JSON Schema compliance testing for root primitives:
|
|
245
|
+
|
|
246
|
+
```ruby
|
|
247
|
+
# In spec/integration/json_schema_compliance_spec.rb
|
|
248
|
+
PositiveInteger = EasyTalk::Primitive(:integer, minimum: 0)
|
|
249
|
+
|
|
250
|
+
# Test cases from JSON Schema Test Suite
|
|
251
|
+
expect(PositiveInteger.valid?(5)).to eq(true)
|
|
252
|
+
expect(PositiveInteger.valid?(-1)).to eq(false)
|
|
253
|
+
expect(PositiveInteger.valid?("foo")).to eq(false)
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
**Implementation for standalone validation:**
|
|
257
|
+
|
|
258
|
+
Standalone validation reuses `ActiveModelAdapter` to ensure identical behavior in both contexts. A lightweight validator class is created once and cached:
|
|
259
|
+
|
|
260
|
+
```ruby
|
|
261
|
+
module EasyTalk
|
|
262
|
+
class Primitive
|
|
263
|
+
class << self
|
|
264
|
+
def call(type_name, **constraints)
|
|
265
|
+
Class.new(self) do
|
|
266
|
+
@base_type = type_name
|
|
267
|
+
@constraints = constraints.freeze
|
|
268
|
+
|
|
269
|
+
class << self
|
|
270
|
+
attr_reader :base_type, :constraints
|
|
271
|
+
|
|
272
|
+
# ... existing schema/ruby_type methods ...
|
|
273
|
+
|
|
274
|
+
# Validate a raw value against this Primitive's schema
|
|
275
|
+
# Returns true if valid, false otherwise
|
|
276
|
+
def valid?(value)
|
|
277
|
+
validator_instance(value).valid?
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
# Returns ActiveModel::Errors-style error messages
|
|
281
|
+
# Example: { value: ["must be a valid email address"] }
|
|
282
|
+
def validate(value)
|
|
283
|
+
instance = validator_instance(value)
|
|
284
|
+
instance.valid?
|
|
285
|
+
instance.errors.to_hash
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
private
|
|
289
|
+
|
|
290
|
+
# Creates a validator instance with the given value
|
|
291
|
+
def validator_instance(value)
|
|
292
|
+
validator_class.new(value)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
# Lazily builds and caches a validator class with ActiveModel validations
|
|
296
|
+
# This class is created once per Primitive subclass
|
|
297
|
+
def validator_class
|
|
298
|
+
@validator_class ||= build_validator_class
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def build_validator_class
|
|
302
|
+
primitive_type = ruby_type
|
|
303
|
+
primitive_constraints = @constraints
|
|
304
|
+
|
|
305
|
+
Class.new do
|
|
306
|
+
include ActiveModel::Validations
|
|
307
|
+
|
|
308
|
+
attr_accessor :value
|
|
309
|
+
|
|
310
|
+
def initialize(value)
|
|
311
|
+
@value = value
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
# Make error messages cleaner (not "Value must be..." but "must be...")
|
|
315
|
+
def self.name
|
|
316
|
+
'PrimitiveValidator'
|
|
317
|
+
end
|
|
318
|
+
end.tap do |klass|
|
|
319
|
+
# Reuse ActiveModelAdapter to apply the same validations
|
|
320
|
+
# used when Primitive is wrapped in a Model
|
|
321
|
+
ValidationAdapters::ActiveModelAdapter.build_validations(
|
|
322
|
+
klass,
|
|
323
|
+
:value,
|
|
324
|
+
primitive_type,
|
|
325
|
+
primitive_constraints
|
|
326
|
+
)
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
**Key benefits of this approach:**
|
|
338
|
+
|
|
339
|
+
1. **Single source of truth** — `ActiveModelAdapter` is the only place validation logic lives
|
|
340
|
+
2. **Identical behavior** — Standalone and Model-wrapped validation produce the same results
|
|
341
|
+
3. **Same error messages** — "must be a valid email address" in both contexts
|
|
342
|
+
4. **No duplication** — No separate constraint validation code to maintain
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
### Feature 2: Symbol-Based Type Syntax
|
|
347
|
+
|
|
348
|
+
#### Overview
|
|
349
|
+
|
|
350
|
+
Allow symbols as type declarations, providing a more JSON Schema-aligned syntax.
|
|
351
|
+
|
|
352
|
+
#### Syntax
|
|
353
|
+
|
|
354
|
+
```ruby
|
|
355
|
+
class User
|
|
356
|
+
include EasyTalk::Model
|
|
357
|
+
|
|
358
|
+
define_schema do
|
|
359
|
+
property :name, :string, min_length: 1
|
|
360
|
+
property :age, :integer, minimum: 0
|
|
361
|
+
property :score, :number
|
|
362
|
+
property :active, :boolean
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
```
|
|
366
|
+
|
|
367
|
+
#### Supported Symbols
|
|
368
|
+
|
|
369
|
+
| Symbol | Maps To |
|
|
370
|
+
|--------|---------|
|
|
371
|
+
| `:string` | `String` |
|
|
372
|
+
| `:integer` | `Integer` |
|
|
373
|
+
| `:number` | `Float` |
|
|
374
|
+
| `:boolean` | `T::Boolean` |
|
|
375
|
+
| `:array` | `Array` |
|
|
376
|
+
| `:null` | `NilClass` |
|
|
377
|
+
|
|
378
|
+
#### Implementation
|
|
379
|
+
|
|
380
|
+
**New File**: `lib/easy_talk/type_resolver.rb`
|
|
381
|
+
|
|
382
|
+
**CRITICAL**: Symbol resolution MUST happen in SchemaDefinition BEFORE the type is passed to:
|
|
383
|
+
1. Property (for JSON Schema generation)
|
|
384
|
+
2. ActiveModelAdapter (for validation generation)
|
|
385
|
+
|
|
386
|
+
Both components expect Ruby classes, not symbols. Passing `:string` instead of `String` will break `type.is_a?(Class)` checks.
|
|
387
|
+
|
|
388
|
+
```ruby
|
|
389
|
+
module EasyTalk
|
|
390
|
+
class TypeResolver
|
|
391
|
+
SYMBOL_TO_TYPE = {
|
|
392
|
+
string: String,
|
|
393
|
+
integer: Integer,
|
|
394
|
+
number: Float,
|
|
395
|
+
boolean: T::Boolean,
|
|
396
|
+
array: Array,
|
|
397
|
+
null: NilClass
|
|
398
|
+
}.freeze
|
|
399
|
+
|
|
400
|
+
class << self
|
|
401
|
+
# Resolves type to a Ruby class
|
|
402
|
+
# @param type [Symbol, Class, Object] the type to resolve
|
|
403
|
+
# @return [Class] the resolved Ruby class
|
|
404
|
+
# @raise [ArgumentError] if symbol is unknown
|
|
405
|
+
def resolve(type)
|
|
406
|
+
case type
|
|
407
|
+
when Symbol
|
|
408
|
+
SYMBOL_TO_TYPE.fetch(type) do
|
|
409
|
+
raise ArgumentError, "Unknown type symbol: #{type.inspect}. Valid symbols: #{SYMBOL_TO_TYPE.keys.join(', ')}"
|
|
410
|
+
end
|
|
411
|
+
when Class
|
|
412
|
+
type
|
|
413
|
+
else
|
|
414
|
+
type # Pass through T:: types, etc.
|
|
415
|
+
end
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# Check if a value is a resolvable symbol
|
|
419
|
+
def symbol_type?(type)
|
|
420
|
+
type.is_a?(Symbol) && SYMBOL_TO_TYPE.key?(type)
|
|
421
|
+
end
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
```
|
|
426
|
+
|
|
427
|
+
---
|
|
428
|
+
|
|
429
|
+
### Feature 3: Convention-Based Type Inference
|
|
430
|
+
|
|
431
|
+
#### Overview
|
|
432
|
+
|
|
433
|
+
Automatically infer property types and constraints based on naming patterns.
|
|
434
|
+
|
|
435
|
+
#### Design Principles
|
|
436
|
+
|
|
437
|
+
- **Conservative defaults** — Only unambiguous, safe patterns
|
|
438
|
+
- **No Float inference for financial fields** — Risk of precision loss
|
|
439
|
+
- **Per-schema override capability** — Full control when needed
|
|
440
|
+
- **Explicit opt-in required** — No magic by default
|
|
441
|
+
|
|
442
|
+
#### Activation
|
|
443
|
+
|
|
444
|
+
**Per-schema** (recommended):
|
|
445
|
+
```ruby
|
|
446
|
+
define_schema do
|
|
447
|
+
infer_types true
|
|
448
|
+
property :email # Inferred as String with format: 'email'
|
|
449
|
+
end
|
|
450
|
+
```
|
|
451
|
+
|
|
452
|
+
**With custom conventions** (merged with global):
|
|
453
|
+
```ruby
|
|
454
|
+
define_schema do
|
|
455
|
+
infer_types conventions: {
|
|
456
|
+
/\A.*_at\z/ => { type: String, constraints: { format: 'date-time' } },
|
|
457
|
+
/\Aurl\z/i => { type: String, constraints: { format: 'uri' } }
|
|
458
|
+
}
|
|
459
|
+
end
|
|
460
|
+
```
|
|
461
|
+
|
|
462
|
+
**With ONLY specific conventions** (ignores global):
|
|
463
|
+
```ruby
|
|
464
|
+
define_schema do
|
|
465
|
+
infer_types only: {
|
|
466
|
+
/\Aemail\z/i => { type: String, constraints: { format: 'email' } }
|
|
467
|
+
}
|
|
468
|
+
end
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
#### Conservative Default Conventions
|
|
472
|
+
|
|
473
|
+
Only patterns that are **unambiguous**, **safe**, and **standard**:
|
|
474
|
+
|
|
475
|
+
| Pattern | Type | Constraints | Why Safe |
|
|
476
|
+
|---------|------|-------------|----------|
|
|
477
|
+
| `/\Aemail\z/i` | String | `format: 'email'` | Always a string, always email format |
|
|
478
|
+
| `/\A(is_\|has_\|can_\|should_\|was_\|will_)/` | T::Boolean | — | Ruby/Rails boolean naming convention |
|
|
479
|
+
| `/\Auuid\z/i` | String | `format: 'uuid'` | Explicit field name, clear intent |
|
|
480
|
+
|
|
481
|
+
#### Intentionally EXCLUDED Patterns
|
|
482
|
+
|
|
483
|
+
| Pattern | Why Excluded |
|
|
484
|
+
|---------|--------------|
|
|
485
|
+
| `price`, `cost`, `total` → Float | **Dangerous for financial data**. Float precision issues cause real bugs. Use Integer (cents) or BigDecimal. |
|
|
486
|
+
| `*_id` → uuid format | Could be integer auto-increment IDs, not UUIDs |
|
|
487
|
+
| `*_count` → Integer | Could be Float for averages |
|
|
488
|
+
| `*_at`, `*_date` → date-time | Could be DateTime objects, not strings |
|
|
489
|
+
|
|
490
|
+
#### Usage Examples
|
|
491
|
+
|
|
492
|
+
```ruby
|
|
493
|
+
# Per-schema activation with conservative global defaults
|
|
494
|
+
class User
|
|
495
|
+
include EasyTalk::Model
|
|
496
|
+
|
|
497
|
+
define_schema do
|
|
498
|
+
infer_types true
|
|
499
|
+
|
|
500
|
+
property :email # Inferred: String, format: 'email'
|
|
501
|
+
property :is_admin # Inferred: T::Boolean (is_ prefix)
|
|
502
|
+
property :has_verified # Inferred: T::Boolean (has_ prefix)
|
|
503
|
+
property :uuid # Inferred: String, format: 'uuid'
|
|
504
|
+
property :name, String # Explicit type - no inference
|
|
505
|
+
property :age, Integer # Explicit type - no inference
|
|
506
|
+
end
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
# Per-schema with custom conventions (merged with global)
|
|
510
|
+
class Event
|
|
511
|
+
include EasyTalk::Model
|
|
512
|
+
|
|
513
|
+
define_schema do
|
|
514
|
+
infer_types conventions: {
|
|
515
|
+
/\A.*_at\z/ => { type: String, constraints: { format: 'date-time' } },
|
|
516
|
+
/\Aurl\z/i => { type: String, constraints: { format: 'uri' } }
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
property :email # From global: String, format: 'email'
|
|
520
|
+
property :created_at # From local: String, format: 'date-time'
|
|
521
|
+
property :url # From local: String, format: 'uri'
|
|
522
|
+
end
|
|
523
|
+
end
|
|
524
|
+
|
|
525
|
+
# Per-schema with ONLY specific conventions (ignores global)
|
|
526
|
+
class Payment
|
|
527
|
+
include EasyTalk::Model
|
|
528
|
+
|
|
529
|
+
define_schema do
|
|
530
|
+
infer_types only: {
|
|
531
|
+
/\Aemail\z/i => { type: String, constraints: { format: 'email' } }
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
property :email # Inferred: String, format: 'email'
|
|
535
|
+
property :is_refunded, T::Boolean # Must be explicit (only: ignores global)
|
|
536
|
+
end
|
|
537
|
+
end
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
#### Implementation
|
|
541
|
+
|
|
542
|
+
**Modified File**: `lib/easy_talk/configuration.rb`
|
|
543
|
+
|
|
544
|
+
```ruby
|
|
545
|
+
class Configuration
|
|
546
|
+
attr_accessor :infer_types
|
|
547
|
+
attr_reader :type_conventions
|
|
548
|
+
|
|
549
|
+
def initialize
|
|
550
|
+
@infer_types = false # Disabled by default
|
|
551
|
+
@type_conventions = default_conventions
|
|
552
|
+
end
|
|
553
|
+
|
|
554
|
+
def register_convention(pattern, type:, **constraints)
|
|
555
|
+
@type_conventions[pattern] = { type: type, constraints: constraints }
|
|
556
|
+
end
|
|
557
|
+
|
|
558
|
+
def clear_conventions!
|
|
559
|
+
@type_conventions = {}
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def reset_conventions!
|
|
563
|
+
@type_conventions = default_conventions
|
|
564
|
+
end
|
|
565
|
+
|
|
566
|
+
private
|
|
567
|
+
|
|
568
|
+
# CONSERVATIVE DEFAULTS: Only include patterns that are:
|
|
569
|
+
# 1. Unambiguous (email is always email-formatted string)
|
|
570
|
+
# 2. Safe (no precision-sensitive types like Float for money)
|
|
571
|
+
# 3. Standard (widely accepted conventions)
|
|
572
|
+
def default_conventions
|
|
573
|
+
{
|
|
574
|
+
/\Aemail\z/i => { type: String, constraints: { format: 'email' } },
|
|
575
|
+
/\A(is_|has_|can_|should_|was_|will_)/ => { type: T::Boolean, constraints: {} },
|
|
576
|
+
/\Auuid\z/i => { type: String, constraints: { format: 'uuid' } }
|
|
577
|
+
}
|
|
578
|
+
end
|
|
579
|
+
end
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
**Modified File**: `lib/easy_talk/schema_definition.rb`
|
|
583
|
+
|
|
584
|
+
**Resolution Order** (CRITICAL for compatibility):
|
|
585
|
+
1. Resolve symbols to Ruby classes (`:string` → `String`)
|
|
586
|
+
2. Infer type from name if no explicit type and inference enabled
|
|
587
|
+
3. Store the resolved Ruby class (never a symbol)
|
|
588
|
+
|
|
589
|
+
```ruby
|
|
590
|
+
class SchemaDefinition
|
|
591
|
+
def initialize(name, options = {})
|
|
592
|
+
# ... existing code ...
|
|
593
|
+
@infer_types_enabled = false
|
|
594
|
+
@local_conventions = nil # nil = use global, hash = use local
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def property(name, type = nil, **constraints, &block)
|
|
598
|
+
# CRITICAL: resolved_type is ALWAYS a Ruby class, never a symbol
|
|
599
|
+
resolved_type, inferred_constraints = resolve_property_type(name, type)
|
|
600
|
+
merged_constraints = inferred_constraints.merge(constraints)
|
|
601
|
+
|
|
602
|
+
# Store the resolved class - Property and ActiveModelAdapter expect classes
|
|
603
|
+
# ... rest of existing property logic ...
|
|
604
|
+
end
|
|
605
|
+
|
|
606
|
+
# Per-schema DSL option with flexible arguments
|
|
607
|
+
def infer_types(enabled_or_options = true)
|
|
608
|
+
case enabled_or_options
|
|
609
|
+
when true
|
|
610
|
+
@infer_types_enabled = true
|
|
611
|
+
@local_conventions = nil # Use global
|
|
612
|
+
when false
|
|
613
|
+
@infer_types_enabled = false
|
|
614
|
+
when Hash
|
|
615
|
+
@infer_types_enabled = true
|
|
616
|
+
if enabled_or_options[:only]
|
|
617
|
+
@local_conventions = enabled_or_options[:only]
|
|
618
|
+
elsif enabled_or_options[:conventions]
|
|
619
|
+
@local_conventions = EasyTalk.configuration.type_conventions
|
|
620
|
+
.merge(enabled_or_options[:conventions])
|
|
621
|
+
end
|
|
622
|
+
end
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
private
|
|
626
|
+
|
|
627
|
+
def resolve_property_type(name, explicit_type)
|
|
628
|
+
if explicit_type
|
|
629
|
+
resolved = resolve_type(explicit_type)
|
|
630
|
+
return [resolved, {}]
|
|
631
|
+
end
|
|
632
|
+
|
|
633
|
+
return [String, {}] unless @infer_types_enabled
|
|
634
|
+
|
|
635
|
+
infer_type_from_name(name)
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def resolve_type(type)
|
|
639
|
+
case type
|
|
640
|
+
when Symbol
|
|
641
|
+
EasyTalk::TypeResolver.resolve(type)
|
|
642
|
+
when Class
|
|
643
|
+
type
|
|
644
|
+
else
|
|
645
|
+
type
|
|
646
|
+
end
|
|
647
|
+
end
|
|
648
|
+
|
|
649
|
+
def infer_type_from_name(name)
|
|
650
|
+
conventions = @local_conventions || EasyTalk.configuration.type_conventions
|
|
651
|
+
|
|
652
|
+
conventions.each do |pattern, config|
|
|
653
|
+
if name.to_s.match?(pattern)
|
|
654
|
+
return [config[:type], config[:constraints] || {}]
|
|
655
|
+
end
|
|
656
|
+
end
|
|
657
|
+
|
|
658
|
+
[String, {}] # Default fallback
|
|
659
|
+
end
|
|
660
|
+
end
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## Files Summary
|
|
666
|
+
|
|
667
|
+
### New Files
|
|
668
|
+
|
|
669
|
+
| File | Purpose |
|
|
670
|
+
|------|---------|
|
|
671
|
+
| `lib/easy_talk/primitive.rb` | Primitive base class with schema generation and standalone validation |
|
|
672
|
+
| `lib/easy_talk/type_resolver.rb` | Symbol → Ruby type resolution |
|
|
673
|
+
| `spec/easy_talk/primitive_spec.rb` | Unit tests for Primitive class (schema + validation) |
|
|
674
|
+
| `spec/easy_talk/type_resolver_spec.rb` | Unit tests for TypeResolver |
|
|
675
|
+
| `spec/easy_talk/type_inference_spec.rb` | Unit tests for type inference |
|
|
676
|
+
|
|
677
|
+
### Modified Files
|
|
678
|
+
|
|
679
|
+
| File | Changes |
|
|
680
|
+
|------|---------|
|
|
681
|
+
| `lib/easy_talk/configuration.rb` | Add `infer_types`, `type_conventions`, convention methods |
|
|
682
|
+
| `lib/easy_talk/schema_definition.rb` | Add type inference, symbol resolution, `infer_types` DSL |
|
|
683
|
+
| `lib/easy_talk/validation_adapters/active_model_adapter.rb` | **CRITICAL**: Unwrap Primitive types for validation generation |
|
|
684
|
+
| `lib/easy_talk.rb` | Require new files |
|
|
685
|
+
| `spec/integration/json_schema_compliance_spec.rb` | Enable root primitive tests (previously skipped) |
|
|
686
|
+
|
|
687
|
+
**Note**: `lib/easy_talk/property.rb` does NOT need modification. It already handles types with `.schema` via duck typing (line 111-114).
|
|
688
|
+
|
|
689
|
+
---
|
|
690
|
+
|
|
691
|
+
## Backward Compatibility
|
|
692
|
+
|
|
693
|
+
This proposal is **fully backward compatible**:
|
|
694
|
+
|
|
695
|
+
| Aspect | Impact |
|
|
696
|
+
|--------|--------|
|
|
697
|
+
| `infer_types` | Defaults to `false` — no behavior change |
|
|
698
|
+
| Existing `property :name, Type` | Works unchanged |
|
|
699
|
+
| Symbol types | New syntax, doesn't affect existing code |
|
|
700
|
+
| Primitive classes | New feature, doesn't affect existing code |
|
|
701
|
+
| Configuration | New options only, existing options unchanged |
|
|
702
|
+
|
|
703
|
+
**No breaking changes.**
|
|
704
|
+
|
|
705
|
+
---
|
|
706
|
+
|
|
707
|
+
## Edge Cases
|
|
708
|
+
|
|
709
|
+
| Scenario | Behavior |
|
|
710
|
+
|----------|----------|
|
|
711
|
+
| Empty convention list | Falls back to `String` |
|
|
712
|
+
| Conflicting conventions | First match wins (hash iteration order) |
|
|
713
|
+
| Primitive with no constraints | Valid — creates type-only schema |
|
|
714
|
+
| Inference disabled globally, enabled per-schema | Per-schema takes precedence |
|
|
715
|
+
| Explicit type with inference enabled | Explicit type always wins |
|
|
716
|
+
| Unknown symbol type | Raises `ArgumentError` with helpful message |
|
|
717
|
+
|
|
718
|
+
---
|
|
719
|
+
|
|
720
|
+
## Test Plan
|
|
721
|
+
|
|
722
|
+
### Unit Tests
|
|
723
|
+
|
|
724
|
+
1. **Primitive class** (`spec/easy_talk/primitive_spec.rb`)
|
|
725
|
+
- Creates valid schema from keyword arguments
|
|
726
|
+
- Delegates constraint validation to underlying builders
|
|
727
|
+
- Raises errors for invalid constraints (via builder's VALID_OPTIONS)
|
|
728
|
+
- Works with both symbol and constant type syntax
|
|
729
|
+
- Generates correct JSON Schema output
|
|
730
|
+
- Exposes `ruby_type` and `constraints` for ActiveModelAdapter
|
|
731
|
+
- Standalone validation via `.valid?` and `.validate` class methods
|
|
732
|
+
|
|
733
|
+
2. **Type inference** (`spec/easy_talk/type_inference_spec.rb`)
|
|
734
|
+
- Default conventions work correctly
|
|
735
|
+
- Custom conventions can be registered
|
|
736
|
+
- Per-schema `infer_types` DSL works
|
|
737
|
+
- `infer_types conventions: {...}` merges with global
|
|
738
|
+
- `infer_types only: {...}` ignores global
|
|
739
|
+
- Explicit types override inference
|
|
740
|
+
- Unknown names default to String
|
|
741
|
+
|
|
742
|
+
3. **Symbol types** (`spec/easy_talk/type_resolver_spec.rb`)
|
|
743
|
+
- All symbol types resolve correctly
|
|
744
|
+
- Unknown symbols raise errors with helpful message
|
|
745
|
+
- Classes pass through unchanged
|
|
746
|
+
|
|
747
|
+
4. **Symbol resolution timing** (CRITICAL)
|
|
748
|
+
- `property :name, :string` resolves to `String` before reaching Property
|
|
749
|
+
- `property :name, :string` resolves to `String` before reaching ActiveModelAdapter
|
|
750
|
+
- Unknown symbols raise `ArgumentError` with helpful message
|
|
751
|
+
|
|
752
|
+
5. **ActiveModel validation** (Model-wrapped context)
|
|
753
|
+
- Primitive constraints generate correct ActiveModel validations
|
|
754
|
+
- `Email` primitive with `format 'email'` validates email format
|
|
755
|
+
- `PositiveInteger` primitive with `minimum 0` rejects negative numbers
|
|
756
|
+
- Property-level constraints override Primitive constraints
|
|
757
|
+
- Both JSON Schema AND ActiveModel validation work correctly
|
|
758
|
+
|
|
759
|
+
6. **Standalone Primitive validation** (direct context)
|
|
760
|
+
- `Primitive.valid?(value)` returns boolean
|
|
761
|
+
- `Primitive.validate(value)` returns hash of error messages
|
|
762
|
+
- Uses same `ActiveModelAdapter` as Model-wrapped context
|
|
763
|
+
- Validator class is cached per Primitive subclass
|
|
764
|
+
|
|
765
|
+
7. **Validation parity** (CRITICAL)
|
|
766
|
+
- Model-wrapped and standalone validation produce identical results
|
|
767
|
+
- Same error messages in both contexts
|
|
768
|
+
- Same constraint behavior (min_length, format, etc.)
|
|
769
|
+
|
|
770
|
+
### Integration Tests
|
|
771
|
+
|
|
772
|
+
- Primitives work in Model properties (Model-wrapped validation)
|
|
773
|
+
- Primitives work standalone (direct validation)
|
|
774
|
+
- **Both contexts produce identical validation results**
|
|
775
|
+
- Inference works with Model properties
|
|
776
|
+
- Symbol types work everywhere
|
|
777
|
+
- Constraints merge correctly (explicit overrides inferred)
|
|
778
|
+
|
|
779
|
+
### JSON Schema Compliance Tests
|
|
780
|
+
|
|
781
|
+
- Root-level integer schemas pass compliance tests
|
|
782
|
+
- Root-level string schemas pass compliance tests
|
|
783
|
+
- Root-level boolean schemas pass compliance tests
|
|
784
|
+
- Root-level number schemas pass compliance tests
|
|
785
|
+
- Constraint combinations work correctly
|
|
786
|
+
|
|
787
|
+
### Edge Cases
|
|
788
|
+
|
|
789
|
+
- Empty convention list
|
|
790
|
+
- Conflicting conventions (first match wins)
|
|
791
|
+
- Primitive with no constraints
|
|
792
|
+
- Inference disabled globally but enabled per-schema
|
|
793
|
+
- Nested models with mixed inference settings
|
|
794
|
+
|
|
795
|
+
---
|
|
796
|
+
|
|
797
|
+
## Design Rationale: Conservative Defaults
|
|
798
|
+
|
|
799
|
+
### Why Minimal Default Conventions?
|
|
800
|
+
|
|
801
|
+
1. **Financial Data Safety**: Inferring `Float` for `price`, `cost`, `total` is dangerous. Float precision issues can cause real financial bugs. Users should explicitly choose `Integer` (cents), `BigDecimal`, or `Float` based on their needs.
|
|
802
|
+
|
|
803
|
+
2. **ID Ambiguity**: `*_id` fields could be:
|
|
804
|
+
- Integer auto-increment IDs
|
|
805
|
+
- UUID strings
|
|
806
|
+
- External system IDs (various formats)
|
|
807
|
+
|
|
808
|
+
Assuming `format: 'uuid'` would break many applications.
|
|
809
|
+
|
|
810
|
+
3. **Count Ambiguity**: `*_count` could be:
|
|
811
|
+
- Integer counts
|
|
812
|
+
- Float averages
|
|
813
|
+
- Decimal for precision
|
|
814
|
+
|
|
815
|
+
4. **Explicit is Better Than Implicit**: Schema definitions should be predictable. Magic that "just works" until it doesn't is worse than requiring explicit types.
|
|
816
|
+
|
|
817
|
+
### Per-Schema Override Philosophy
|
|
818
|
+
|
|
819
|
+
- `infer_types true` — Opt-in to global conventions
|
|
820
|
+
- `infer_types conventions: {...}` — Add project-specific patterns
|
|
821
|
+
- `infer_types only: {...}` — Full control, no surprises from global config
|
|
822
|
+
|
|
823
|
+
This ensures models remain isolated and predictable.
|
|
824
|
+
|
|
825
|
+
---
|
|
826
|
+
|
|
827
|
+
## Alternatives Considered
|
|
828
|
+
|
|
829
|
+
### Alternative 1: Schema-Only Mode via Class Method
|
|
830
|
+
|
|
831
|
+
```ruby
|
|
832
|
+
EasyTalk.schema(:string, min_length: 1, format: 'email')
|
|
833
|
+
# => { "type": "string", "minLength": 1, "format": "email" }
|
|
834
|
+
```
|
|
835
|
+
|
|
836
|
+
**Rejected**: Produces schemas but not reusable types. Doesn't integrate with Model properties.
|
|
837
|
+
|
|
838
|
+
### Alternative 2: Anonymous Property Builder
|
|
839
|
+
|
|
840
|
+
```ruby
|
|
841
|
+
EasyTalk::Property.build(:value, String, format: 'email').schema
|
|
842
|
+
```
|
|
843
|
+
|
|
844
|
+
**Rejected**: More verbose than Primitive classes. Awkward API for reuse.
|
|
845
|
+
|
|
846
|
+
### Alternative 3: Type inference only (no Primitives)
|
|
847
|
+
|
|
848
|
+
**Rejected**: Primitives provide explicit, documented types that are easier to understand and test than convention-based inference alone.
|
|
849
|
+
|
|
850
|
+
### Alternative 4: Aggressive default conventions
|
|
851
|
+
|
|
852
|
+
**Rejected**: Patterns like `price` → `Float` are dangerous for financial applications. Conservative defaults prevent subtle bugs.
|
|
853
|
+
|
|
854
|
+
### Alternative 5: Block DSL with ConstraintCollector
|
|
855
|
+
|
|
856
|
+
```ruby
|
|
857
|
+
class Email < EasyTalk::Primitive(:string) { format 'email' }
|
|
858
|
+
```
|
|
859
|
+
|
|
860
|
+
**Rejected**: Creates duplication with builder `VALID_OPTIONS`. Each builder already defines valid constraints with type checking. A separate `ConstraintCollector` class would need to mirror all constraint methods (`format`, `pattern`, `minimum`, etc.) and keep them in sync with builders. Keyword arguments are simpler and leverage existing validation.
|
|
861
|
+
|
|
862
|
+
---
|
|
863
|
+
|
|
864
|
+
## Implementation Order
|
|
865
|
+
|
|
866
|
+
1. **Phase 1**: TypeResolver + symbol types (smallest scope, foundational)
|
|
867
|
+
2. **Phase 2**: Primitive class (builds on Phase 1)
|
|
868
|
+
3. **Phase 3**: ActiveModelAdapter Primitive support (CRITICAL)
|
|
869
|
+
4. **Phase 4**: Convention registry in Configuration
|
|
870
|
+
5. **Phase 5**: Type inference in SchemaDefinition
|
|
871
|
+
6. **Phase 6**: Documentation and comprehensive tests
|
|
872
|
+
|
|
873
|
+
---
|
|
874
|
+
|
|
875
|
+
## Open Questions
|
|
876
|
+
|
|
877
|
+
1. **Convention ordering**: Should we use an ordered data structure instead of Hash to ensure deterministic matching?
|
|
878
|
+
|
|
879
|
+
2. **Primitive composition**: Should Primitives support composing with other Primitives?
|
|
880
|
+
```ruby
|
|
881
|
+
class NonEmptyEmail < EasyTalk::Primitive(Email, min_length: 1)
|
|
882
|
+
```
|
|
883
|
+
|
|
884
|
+
3. **Array item type inference**: Should `property :emails` infer `T::Array[Email]`?
|
|
885
|
+
|
|
886
|
+
4. **Symbols in T:: types**: Should `T::Array[:string]` work, or raise an error?
|
|
887
|
+
|
|
888
|
+
---
|
|
889
|
+
|
|
890
|
+
## References
|
|
891
|
+
|
|
892
|
+
- [Pydantic Field Types](https://docs.pydantic.dev/latest/concepts/types/) — Inspiration for Primitive concept
|
|
893
|
+
- [JSON Schema Specification](https://json-schema.org/specification.html)
|
|
894
|
+
- [Rails Convention over Configuration](https://rubyonrails.org/doctrine#convention-over-configuration)
|