easy_talk 3.3.0 → 3.3.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: 3f11b7be6e0fa32087d5be57be3d3e37d5bd5b96cc61ae8626b33d41a420e401
4
- data.tar.gz: 82ba98f220e8e75e3bb3e208a595a168f188c226a130e88f13991da9a386cfc6
3
+ metadata.gz: b571b59c30a196b82285a7fa67c275002cb9f4bc6a07a0e0c70cc69d04d60b50
4
+ data.tar.gz: 599c489167248facc490dbc3add9c0fddb005b9aab654d919e0377a1ae2352af
5
5
  SHA512:
6
- metadata.gz: 90fb93ef486adb8151e883e6929437be2f52bdbe4a51bb45485006884a983b80ce9f10de1d14b313031be4b8126d6fe21c3bd54a998863543a1be1d752c124d9
7
- data.tar.gz: 2ff9102ba57ae6738cb79511fdd095b5f798a760c009b10d2b5577c30575f04d94dfb11e410036e566971d92e27e2c4bc2d17336e44c64f9d35b966bbe1b4960
6
+ metadata.gz: 8f27233da948ebf61071d179f8b8b1031df4ffa262bd9050488aa09e82e462d8c8a52b25290b8af1965c7c23f39c73a43c8b95972ac3d388aea645c50d7754c9
7
+ data.tar.gz: 8cba0f544c9746674f15005232c714d385a3e407c3add6d4089eb2260a1a6889a0e7a0160283d42fdc388f91c26ac96c52d8fae8147458821cd346bde2c60651
data/CHANGELOG.md CHANGED
@@ -1,3 +1,44 @@
1
+ ## [3.3.2] - 2026-03-09
2
+
3
+ ### Fixed
4
+
5
+ - **Schema Cache Corruption**: Fixed schema cache corruption when nested models have constraints (#163)
6
+ - **Mutable Default Values (Model)**: Fixed mutable default values (arrays, hashes) being shared across `EasyTalk::Model` instances (#165)
7
+ - **Nilable Properties Rejecting Nil**: Fixed nilable properties incorrectly rejecting `nil` in ActiveModel validations (#166)
8
+ - **Nested Model Blank False Positive**: Fixed nested model blank false positive in `apply_object_validations` (#167)
9
+ - **Nilable Boolean Validation**: Fixed `T.nilable(T::Boolean)` rejecting `nil` in ActiveModel validations (#168)
10
+ - **Schema False Value Discard**: Fixed `EasyTalk::Schema` silently discarding `false` values on initialization (#169)
11
+ - **Mutable Default Values (Schema)**: Fixed mutable default values shared across `EasyTalk::Schema` instances (#170)
12
+ - **Double-Call Stale Schema**: Fixed `define_schema` double-call leaving stale schema and validators (#171)
13
+ - **Incorrect Fallback String**: Fixed incorrect fallback string in type introspection for composition types (#175)
14
+ - **Optional Array Nil Rejection**: Fixed optional array properties incorrectly rejecting `nil` (#177)
15
+
16
+ ### Added
17
+
18
+ - **Option Validation for UnionBuilder and NullBuilder**: Added constraint option validation to `UnionBuilder` and `NullBuilder`, raising errors on unsupported options (#176)
19
+
20
+ ### Internal
21
+
22
+ - **SchemaBase Extraction**: Extracted `SchemaBase` module to eliminate duplication between `Model` and `Schema` (#172)
23
+ - **Type Introspection Consolidation**: Consolidated type introspection logic into the `TypeIntrospection` module, removing duplicate checks from adapters and helpers (#173, #174)
24
+ - **Test Suite Consolidation**: Consolidated standalone bug-fix specs into the regular test suite for better organization
25
+
26
+ ## [3.3.1] - 2026-02-03
27
+
28
+ ### Added
29
+
30
+ - **RubyLLM Compatibility Extension**: Seamless integration between EasyTalk models and RubyLLM's tool and structured output features (#122)
31
+ - New `RubyLLMCompatibility` module adds class method `to_json_schema` for `with_schema` support
32
+ - New `RubyLLMToolOverrides` module for classes inheriting from `RubyLLM::Tool`
33
+ - Overrides `description` and `params_schema` methods to use EasyTalk schema definitions
34
+ - Automatically included when using `EasyTalk::Model`
35
+ - Tools inherit from `RubyLLM::Tool` directly, gaining full access to features like `halt`, `call`, `provider_params`
36
+ - New example files: `examples/ruby_llm/tools_integration.rb`, `examples/ruby_llm/structured_output.rb`
37
+
38
+ ### Changed
39
+
40
+ - **Documentation**: Updated README with improved examples and documentation
41
+
1
42
  ## [3.3.0] - 2026-01-12
2
43
 
3
44
  ### Added
data/README.md CHANGED
@@ -52,13 +52,43 @@ EasyTalk makes the schema definition the single source of truth, so you can:
52
52
  - **RFC 7807** problem details
53
53
  - **JSON:API** error objects
54
54
 
55
- - **LLM tool/function schemas without a second schema layer**
56
- Use the same contract to generate JSON Schema for function/tool calling.
55
+ - **LLM tool/function schemas without a second schema layer**
56
+ Use the same contract to generate JSON Schema for function/tool calling. See [RubyLLM Integration](#rubyllm-integration).
57
57
 
58
58
  EasyTalk is for teams who want their data contracts to be **correct, reusable, and boring** (the good kind of boring).
59
59
 
60
60
  ---
61
61
 
62
+ ## Table of Contents
63
+
64
+ - [Installation](#installation)
65
+ - [Quick start](#quick-start)
66
+ - [Property constraints](#property-constraints)
67
+ - [Core concepts](#core-concepts)
68
+ - [Required vs optional vs nullable](#required-vs-optional-vs-nullable-dont-get-tricked)
69
+ - [Nested models](#nested-models-and-automatic-instantiation)
70
+ - [Tuple arrays](#tuple-arrays-fixed-position-types)
71
+ - [Composition (AnyOf / OneOf / AllOf)](#composition-anyof--oneof--allof)
72
+ - [Validations](#validations)
73
+ - [Automatic validations](#automatic-validations-default)
74
+ - [Per-model validation control](#per-model-validation-control)
75
+ - [Per-property validation control](#per-property-validation-control)
76
+ - [Validation adapters](#validation-adapters)
77
+ - [Error formatting](#error-formatting)
78
+ - [Schema-only mode](#schema-only-mode)
79
+ - [RubyLLM Integration](#rubyllm-integration)
80
+ - [Configuration highlights](#configuration-highlights)
81
+ - [Advanced topics](#advanced-topics)
82
+ - [JSON Schema drafts, `$id`, and `$ref`](#json-schema-drafts-id-and-ref)
83
+ - [Additional properties with types](#additional-properties-with-types)
84
+ - [Object-level constraints](#object-level-constraints)
85
+ - [Custom type builders](#custom-type-builders)
86
+ - [Known limitations](#known-limitations)
87
+ - [Contributing](#contributing)
88
+ - [License](#license)
89
+
90
+ ---
91
+
62
92
  ## Installation
63
93
 
64
94
  ### Requirements
@@ -80,6 +110,14 @@ bundle install
80
110
 
81
111
  ## Quick start
82
112
 
113
+ <table>
114
+ <tr>
115
+ <th>EasyTalk Model</th>
116
+ <th>Generated JSON Schema</th>
117
+ </tr>
118
+ <tr>
119
+ <td>
120
+
83
121
  ```ruby
84
122
  require "easy_talk"
85
123
 
@@ -96,14 +134,10 @@ class User
96
134
  property :age, Integer, minimum: 18
97
135
  end
98
136
  end
99
-
100
- User.json_schema # => Ruby Hash (JSON Schema)
101
- user = User.new(name: "A") # invalid: min_length is 2
102
- user.valid? # => false
103
- user.errors # => ActiveModel::Errors
104
137
  ```
105
138
 
106
- **Generated JSON Schema:**
139
+ </td>
140
+ <td>
107
141
 
108
142
  ```json
109
143
  {
@@ -120,6 +154,17 @@ user.errors # => ActiveModel::Errors
120
154
  }
121
155
  ```
122
156
 
157
+ </td>
158
+ </tr>
159
+ </table>
160
+
161
+ ```ruby
162
+ User.json_schema # => Ruby Hash (JSON Schema)
163
+ user = User.new(name: "A") # invalid: min_length is 2
164
+ user.valid? # => false
165
+ user.errors # => ActiveModel::Errors
166
+ ```
167
+
123
168
  ---
124
169
 
125
170
  ## Property constraints
@@ -240,6 +285,14 @@ end
240
285
 
241
286
  Use `T::Tuple` for arrays where each position has a specific type (e.g., coordinates, CSV rows, database records):
242
287
 
288
+ <table>
289
+ <tr>
290
+ <th>EasyTalk Model</th>
291
+ <th>Generated JSON Schema</th>
292
+ </tr>
293
+ <tr>
294
+ <td>
295
+
243
296
  ```ruby
244
297
  class GeoLocation
245
298
  include EasyTalk::Model
@@ -257,7 +310,8 @@ location = GeoLocation.new(
257
310
  )
258
311
  ```
259
312
 
260
- **Generated JSON Schema:**
313
+ </td>
314
+ <td>
261
315
 
262
316
  ```json
263
317
  {
@@ -273,6 +327,10 @@ location = GeoLocation.new(
273
327
  }
274
328
  ```
275
329
 
330
+ </td>
331
+ </tr>
332
+ </table>
333
+
276
334
  **Mixed-type tuples:**
277
335
 
278
336
  ```ruby
@@ -469,6 +527,66 @@ Use this for documentation, OpenAPI generation, or when validation happens elsew
469
527
 
470
528
  ---
471
529
 
530
+ ## RubyLLM Integration
531
+
532
+ EasyTalk integrates seamlessly with [RubyLLM](https://github.com/crmne/ruby_llm) for structured outputs and tool definitions.
533
+
534
+ ### Structured Outputs
535
+
536
+ Use any EasyTalk model with RubyLLM's `with_schema` to get structured JSON responses:
537
+
538
+ ```ruby
539
+ class Recipe
540
+ include EasyTalk::Model
541
+
542
+ define_schema do
543
+ description "A cooking recipe"
544
+ property :name, String, description: "Name of the dish"
545
+ property :ingredients, T::Array[String], description: "List of ingredients"
546
+ property :prep_time_minutes, Integer, description: "Preparation time in minutes"
547
+ end
548
+ end
549
+
550
+ chat = RubyLLM.chat.with_schema(Recipe)
551
+ response = chat.ask("Give me a simple pasta recipe")
552
+
553
+ # RubyLLM returns parsed JSON - instantiate with EasyTalk model
554
+ recipe = Recipe.new(response.content)
555
+ recipe.name # => "Spaghetti Aglio e Olio"
556
+ recipe.ingredients # => ["spaghetti", "garlic", "olive oil", ...]
557
+ ```
558
+
559
+ ### Tools
560
+
561
+ Create LLM tools by inheriting from `RubyLLM::Tool` and including `EasyTalk::Model`:
562
+
563
+ ```ruby
564
+ class Weather < RubyLLM::Tool
565
+ include EasyTalk::Model
566
+
567
+ define_schema do
568
+ description 'Gets current weather for a location'
569
+ property :latitude, String, description: 'Latitude (e.g., 52.5200)'
570
+ property :longitude, String, description: 'Longitude (e.g., 13.4050)'
571
+ end
572
+
573
+ def execute(latitude:, longitude:)
574
+ # Fetch weather data from API...
575
+ { temperature: 22, conditions: "sunny" }
576
+ end
577
+ end
578
+
579
+ chat = RubyLLM.chat.with_tool(Weather)
580
+ response = chat.ask("What's the weather in Berlin?")
581
+ ```
582
+
583
+ This pattern gives you:
584
+ - Full access to `RubyLLM::Tool` features (`halt`, `call`, etc.)
585
+ - EasyTalk's schema DSL for parameter definitions
586
+ - Automatic JSON Schema generation for the LLM
587
+
588
+ ---
589
+
472
590
  ## Configuration highlights
473
591
 
474
592
  ```ruby
@@ -517,6 +635,14 @@ end
517
635
 
518
636
  Use external URIs in `$ref` for modular, reusable schemas:
519
637
 
638
+ <table>
639
+ <tr>
640
+ <th>EasyTalk Model</th>
641
+ <th>Generated JSON Schema</th>
642
+ </tr>
643
+ <tr>
644
+ <td>
645
+
520
646
  ```ruby
521
647
  EasyTalk.configure do |config|
522
648
  config.use_refs = true
@@ -544,20 +670,34 @@ class Customer
544
670
  end
545
671
 
546
672
  Customer.json_schema
547
- # =>
548
- # {
549
- # "properties": {
550
- # "address": { "$ref": "https://example.com/schemas/address" }
551
- # },
552
- # "$defs": {
553
- # "Address": {
554
- # "$id": "https://example.com/schemas/address",
555
- # "properties": { "street": {...}, "city": {...} }
556
- # }
557
- # }
558
- # }
559
673
  ```
560
674
 
675
+ </td>
676
+ <td>
677
+
678
+ ```json
679
+ {
680
+ "properties": {
681
+ "address": {
682
+ "$ref": "https://example.com/schemas/address"
683
+ }
684
+ },
685
+ "$defs": {
686
+ "Address": {
687
+ "$id": "https://example.com/schemas/address",
688
+ "properties": {
689
+ "street": { "type": "string" },
690
+ "city": { "type": "string" }
691
+ }
692
+ }
693
+ }
694
+ }
695
+ ```
696
+
697
+ </td>
698
+ </tr>
699
+ </table>
700
+
561
701
  **Explicit schema IDs:**
562
702
 
563
703
  ```ruby
@@ -608,6 +748,14 @@ config.as_json
608
748
 
609
749
  **With constraints:**
610
750
 
751
+ <table>
752
+ <tr>
753
+ <th>EasyTalk Model</th>
754
+ <th>Generated JSON Schema</th>
755
+ </tr>
756
+ <tr>
757
+ <td>
758
+
611
759
  ```ruby
612
760
  class StrictConfig
613
761
  include EasyTalk::Model
@@ -615,22 +763,34 @@ class StrictConfig
615
763
  define_schema do
616
764
  property :id, Integer
617
765
  # Integer values between 0 and 100 only
618
- additional_properties Integer, minimum: 0, maximum: 100
766
+ additional_properties Integer,
767
+ minimum: 0, maximum: 100
619
768
  end
620
769
  end
621
770
 
622
771
  StrictConfig.json_schema
623
- # =>
624
- # {
625
- # "properties": { "id": { "type": "integer" } },
626
- # "additionalProperties": {
627
- # "type": "integer",
628
- # "minimum": 0,
629
- # "maximum": 100
630
- # }
631
- # }
632
772
  ```
633
773
 
774
+ </td>
775
+ <td>
776
+
777
+ ```json
778
+ {
779
+ "properties": {
780
+ "id": { "type": "integer" }
781
+ },
782
+ "additionalProperties": {
783
+ "type": "integer",
784
+ "minimum": 0,
785
+ "maximum": 100
786
+ }
787
+ }
788
+ ```
789
+
790
+ </td>
791
+ </tr>
792
+ </table>
793
+
634
794
  **Nested models as additional properties:**
635
795
 
636
796
  ```ruby
data/easy_talk.gemspec ADDED
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/easy_talk/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'easy_talk'
7
+ spec.version = EasyTalk::VERSION
8
+ spec.authors = ['Sergio Bayona']
9
+ spec.email = ['bayona.sergio@gmail.com']
10
+
11
+ spec.summary = 'JSON Schema generation and validation for Ruby classes, ideal for LLM function calling.'
12
+ spec.description = 'Define schemas using a clean DSL and get both JSON Schema documents and runtime validations. ' \
13
+ 'Perfect for API request/response validation, LLM function definitions (OpenAI, Anthropic), ' \
14
+ 'and structured data modeling. Features Sorbet-style types, schema composition, ' \
15
+ 'pluggable validation adapters, and multiple error output formats (JSON:API, RFC 7807).'
16
+ spec.homepage = 'https://github.com/sergiobayona/easy_talk'
17
+ spec.license = 'MIT'
18
+ spec.required_ruby_version = '>= 3.2'
19
+
20
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org'
21
+
22
+ spec.metadata['homepage_uri'] = spec.homepage
23
+ spec.metadata['changelog_uri'] = 'https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md'
24
+
25
+ # Specify which files should be added to the gem when it is released.
26
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
27
+ spec.files = Dir.chdir(__dir__) do
28
+ `git ls-files -z`.split("\x0").reject do |f|
29
+ (File.expand_path(f) == __FILE__) ||
30
+ f.start_with?(*%w[bin/ spec/ .git .github Gemfile])
31
+ end
32
+ end
33
+
34
+ spec.require_paths = ['lib']
35
+
36
+ spec.add_dependency 'activemodel', ['>= 7.0', '< 9.0']
37
+ spec.add_dependency 'activesupport', ['>= 7.0', '< 9.0']
38
+ spec.add_dependency 'js_regex', '~> 3.0'
39
+ spec.add_dependency 'sorbet-runtime', '~> 0.5'
40
+
41
+ spec.metadata['rubygems_mfa_required'] = 'true'
42
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # EasyTalk from the parent directory
6
+ gem 'easy_talk', path: '../..'
7
+
8
+ # RubyLLM for LLM interactions
9
+ gem 'ruby_llm'
10
+
11
+ # HTTP client for API calls (used in tools example)
12
+ gem 'faraday'
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'ruby_llm'
5
+ require 'easy_talk'
6
+
7
+ # Example: Structured Outputs
8
+ # Demonstrates using EasyTalk models to generate structured JSON responses.
9
+
10
+ RubyLLM.configure do |config|
11
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
12
+ end
13
+
14
+ # 1. Define the Schema using EasyTalk
15
+ class Recipe
16
+ include EasyTalk::Model
17
+
18
+ define_schema do
19
+ description "A simple cooking recipe"
20
+ property :name, String, description: "Name of the dish"
21
+ property :ingredients, T::Array[String], description: "List of ingredients"
22
+ property :prep_time_minutes, Integer, description: "Preparation time in minutes"
23
+ property :steps, T::Array[String], description: "Step by step cooking instructions"
24
+ end
25
+ end
26
+
27
+ puts "--- Structured Output Example ---"
28
+
29
+ # 2. Use the EasyTalk model as the output schema
30
+ # RubyLLM uses the schema to force the LLM to reply with a matching JSON structure.
31
+ # Our compatibility layer ensures 'Recipe' responds to to_json_schema as RubyLLM expects.
32
+ chat = RubyLLM.chat.with_schema(Recipe)
33
+
34
+ puts "User: Give me a simple spaghetti carbonara recipe."
35
+ response = chat.ask "Give me a simple spaghetti carbonara recipe."
36
+
37
+ # 3. Access the structured data
38
+ # RubyLLM returns parsed JSON as a Hash, so we instantiate the model with it
39
+ recipe = Recipe.new(response.content)
40
+
41
+ puts "\nGenerated Recipe:"
42
+ puts "Name: #{recipe.name}"
43
+ puts "Time: #{recipe.prep_time_minutes} mins"
44
+ puts "Ingredients:"
45
+ recipe.ingredients.each { |ing| puts "- #{ing}" }
46
+ puts "Steps:"
47
+ recipe.steps.each_with_index { |step, i| puts "#{i + 1}. #{step}" }
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/setup'
4
+ require 'ruby_llm'
5
+ require 'easy_talk'
6
+ require 'faraday'
7
+
8
+ # Example: Tools Integration
9
+ # Demonstrates using EasyTalk models as Tools for RubyLLM.
10
+ #
11
+ # To create a tool, inherit from RubyLLM::Tool and include EasyTalk::Model.
12
+ # This gives you:
13
+ # - Full access to RubyLLM::Tool features like halt()
14
+ # - EasyTalk's schema DSL for defining parameters
15
+ # - Automatic integration with RubyLLM's with_tool method
16
+
17
+ RubyLLM.configure do |config|
18
+ config.openai_api_key = ENV.fetch('OPENAI_API_KEY', nil)
19
+ end
20
+
21
+ class Weather < RubyLLM::Tool
22
+ include EasyTalk::Model
23
+
24
+ define_schema do
25
+ description 'Gets current weather for a location'
26
+ property :latitude, String, description: 'Latitude (e.g., 52.5200)'
27
+ property :longitude, String, description: 'Longitude (e.g., 13.4050)'
28
+ end
29
+
30
+ def execute(latitude:, longitude:)
31
+ puts "Executing Weather Tool for #{latitude}, #{longitude}"
32
+ url = "https://api.open-meteo.com/v1/forecast?latitude=#{latitude}&longitude=#{longitude}&current=temperature_2m,wind_speed_10m"
33
+ response = Faraday.get(url)
34
+ data = JSON.parse(response.body)
35
+ data.to_s
36
+ rescue StandardError => e
37
+ { error: e.message }
38
+ end
39
+ end
40
+
41
+ puts '--- Tools Integration Example ---'
42
+ puts
43
+
44
+ chat = RubyLLM.chat.with_tool(Weather)
45
+
46
+ puts 'User: What is the weather in Berlin (Lat: 52.52, Long: 13.405)?'
47
+ response = chat.ask 'What is the weather in Berlin (Lat: 52.52, Long: 13.405)?'
48
+
49
+ puts "Assistant: #{response.content}"
@@ -17,6 +17,8 @@ module EasyTalk
17
17
  'OneOfBuilder' => 'oneOf'
18
18
  }.freeze
19
19
 
20
+ VALID_OPTIONS = %i[title description optional as validate ref].freeze
21
+
20
22
  sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
21
23
  # Initializes a new instance of the CompositionBuilder class.
22
24
  #
@@ -24,6 +26,7 @@ module EasyTalk
24
26
  # @param type [Class] The type of the composition.
25
27
  # @param constraints [Hash] The constraints for the composition.
26
28
  def initialize(name, type, constraints)
29
+ EasyTalk.assert_valid_property_options(name, constraints, VALID_OPTIONS)
27
30
  @composer_type = self.class.name.split('::').last
28
31
  @name = name
29
32
  @type = type
@@ -9,10 +9,12 @@ module EasyTalk
9
9
  class NullBuilder < BaseBuilder
10
10
  extend T::Sig
11
11
 
12
+ VALID_OPTIONS = {}.freeze
13
+
12
14
  # Initializes a new instance of the NullBuilder class.
13
- sig { params(name: Symbol, _constraints: T::Hash[Symbol, T.untyped]).void }
14
- def initialize(name, _constraints = {})
15
- super(name, { type: 'null' }, {}, {})
15
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
16
+ def initialize(name, constraints = {})
17
+ super(name, { type: 'null' }, constraints, VALID_OPTIONS)
16
18
  end
17
19
  end
18
20
  end
@@ -41,7 +41,7 @@ module EasyTalk
41
41
  # Keep a reference to the original schema definition
42
42
  @schema_definition = schema_definition
43
43
  # Deep duplicate the raw schema hash so we can mutate it safely
44
- @original_schema = deep_dup(schema_definition.schema)
44
+ @original_schema = EasyTalk.deep_dup(schema_definition.schema)
45
45
 
46
46
  # We'll collect required property names in this Set
47
47
  @required_properties = Set.new
@@ -71,24 +71,6 @@ module EasyTalk
71
71
 
72
72
  private
73
73
 
74
- ##
75
- # Deep duplicates a hash, including nested hashes.
76
- # This prevents mutations from leaking back to the original schema.
77
- #
78
- def deep_dup(obj)
79
- case obj
80
- when Hash
81
- obj.transform_values { |v| deep_dup(v) }
82
- when Array
83
- obj.map { |v| deep_dup(v) }
84
- when Class, Module
85
- # Don't duplicate Class or Module objects - they represent types
86
- obj
87
- else
88
- obj.duplicable? ? obj.dup : obj
89
- end
90
- end
91
-
92
74
  ##
93
75
  # Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
94
76
  # into a single hash that we'll feed to BaseBuilder.
@@ -250,7 +232,7 @@ module EasyTalk
250
232
  elsif prop_type.is_a?(EasyTalk::Types::Composer)
251
233
  collect_ref_models(prop_type.items, constraints)
252
234
  # Handle typed arrays with EasyTalk model items
253
- elsif typed_array?(prop_type)
235
+ elsif TypeIntrospection.typed_array?(prop_type)
254
236
  extract_inner_types(prop_type).each { |inner_type| collect_ref_models(inner_type, constraints) }
255
237
  # Handle nilable types
256
238
  elsif nilable_with_model?(prop_type)
@@ -272,12 +254,8 @@ module EasyTalk
272
254
  EasyTalk.configuration.use_refs
273
255
  end
274
256
 
275
- def typed_array?(prop_type)
276
- prop_type.is_a?(T::Types::TypedArray)
277
- end
278
-
279
257
  def extract_inner_types(prop_type)
280
- return [] unless typed_array?(prop_type)
258
+ return [] unless TypeIntrospection.typed_array?(prop_type)
281
259
 
282
260
  if prop_type.type.is_a?(EasyTalk::Types::Composer)
283
261
  prop_type.type.items
@@ -302,9 +280,7 @@ module EasyTalk
302
280
  # Adds $defs entries for all collected ref models.
303
281
  #
304
282
  def add_ref_model_defs(schema_hash)
305
- definitions = @ref_models.each_with_object({}) do |model, acc|
306
- acc[model.name] = model.schema
307
- end
283
+ definitions = @ref_models.to_h { |model| [model.name, EasyTalk.deep_dup(model.schema)] }
308
284
 
309
285
  existing_defs = schema_hash[:defs] || {}
310
286
  schema_hash[:defs] = existing_defs.merge(definitions)
@@ -327,9 +303,7 @@ module EasyTalk
327
303
  #
328
304
  def add_defs_from_subschema(schema_hash, subschema)
329
305
  # Build up a hash of class_name => schema for each sub-item
330
- definitions = subschema.items.each_with_object({}) do |item, acc|
331
- acc[item.name] = item.schema
332
- end
306
+ definitions = subschema.items.to_h { |item| [item.name, EasyTalk.deep_dup(item.schema)] }
333
307
  # Merge or create :defs
334
308
  existing_defs = schema_hash[:defs] || {}
335
309
  schema_hash[:defs] = existing_defs.merge(definitions)
@@ -10,8 +10,11 @@ module EasyTalk
10
10
  extend CollectionHelpers
11
11
  extend T::Sig
12
12
 
13
+ VALID_OPTIONS = %i[title description optional as validate].freeze
14
+
13
15
  sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
14
16
  def initialize(name, type, constraints)
17
+ EasyTalk.assert_valid_property_options(name, constraints, VALID_OPTIONS)
15
18
  @name = name
16
19
  @type = type
17
20
  @constraints = constraints
@@ -25,7 +25,7 @@ module EasyTalk
25
25
  raise UnknownOptionError, message
26
26
  end
27
27
 
28
- def self.extract_inner_type(type_info)
28
+ def self.extract_element_type(type_info)
29
29
  # No change needed here
30
30
  if type_info.respond_to?(:type) && type_info.type.respond_to?(:raw_type)
31
31
  type_info.type.raw_type
@@ -49,7 +49,7 @@ module EasyTalk
49
49
  def self.validate_typed_array_values(property_name:, constraint_name:, type_info:, array_value:)
50
50
  # Raise error if value is not an array but type expects one
51
51
  unless array_value.is_a?(Array)
52
- inner_type = extract_inner_type(type_info)
52
+ inner_type = extract_element_type(type_info)
53
53
  expected_desc = TypeIntrospection.boolean_type?(inner_type) ? 'Boolean (true or false)' : inner_type.to_s
54
54
  raise_constraint_error(
55
55
  property_name: property_name,
@@ -59,7 +59,7 @@ module EasyTalk
59
59
  )
60
60
  end
61
61
 
62
- inner_type = extract_inner_type(type_info)
62
+ inner_type = extract_element_type(type_info)
63
63
  array_value.each_with_index do |element, index|
64
64
  validate_array_element(
65
65
  property_name: property_name,
@@ -153,7 +153,7 @@ module EasyTalk
153
153
  # Handle Sorbet type objects
154
154
  elsif value_type.class.ancestors.include?(T::Types::Base)
155
155
  # Extract the inner type
156
- inner_type = extract_inner_type(value_type)
156
+ inner_type = extract_element_type(value_type)
157
157
 
158
158
  if inner_type.is_a?(Array)
159
159
  # For union types, check if the value matches any of the allowed types