easy_talk 3.2.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.
Files changed (44) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -43
  3. data/CHANGELOG.md +89 -0
  4. data/README.md +447 -2115
  5. data/docs/json_schema_compliance.md +140 -26
  6. data/docs/primitive-schema-rfc.md +894 -0
  7. data/lib/easy_talk/builders/base_builder.rb +2 -1
  8. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  9. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  10. data/lib/easy_talk/builders/composition_builder.rb +7 -2
  11. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  12. data/lib/easy_talk/builders/null_builder.rb +4 -1
  13. data/lib/easy_talk/builders/number_builder.rb +4 -1
  14. data/lib/easy_talk/builders/object_builder.rb +64 -3
  15. data/lib/easy_talk/builders/registry.rb +15 -1
  16. data/lib/easy_talk/builders/string_builder.rb +3 -1
  17. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  18. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  19. data/lib/easy_talk/builders/typed_array_builder.rb +4 -2
  20. data/lib/easy_talk/builders/union_builder.rb +5 -1
  21. data/lib/easy_talk/configuration.rb +17 -2
  22. data/lib/easy_talk/errors.rb +1 -0
  23. data/lib/easy_talk/errors_helper.rb +3 -0
  24. data/lib/easy_talk/json_schema_equality.rb +46 -0
  25. data/lib/easy_talk/keywords.rb +0 -1
  26. data/lib/easy_talk/model.rb +27 -1
  27. data/lib/easy_talk/model_helper.rb +4 -0
  28. data/lib/easy_talk/naming_strategies.rb +4 -0
  29. data/lib/easy_talk/property.rb +7 -0
  30. data/lib/easy_talk/ref_helper.rb +6 -0
  31. data/lib/easy_talk/schema.rb +1 -0
  32. data/lib/easy_talk/schema_definition.rb +52 -6
  33. data/lib/easy_talk/schema_methods.rb +36 -5
  34. data/lib/easy_talk/sorbet_extension.rb +1 -0
  35. data/lib/easy_talk/type_introspection.rb +45 -1
  36. data/lib/easy_talk/types/tuple.rb +77 -0
  37. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +350 -62
  38. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  39. data/lib/easy_talk/validation_adapters/base.rb +12 -0
  40. data/lib/easy_talk/validation_adapters/none_adapter.rb +9 -0
  41. data/lib/easy_talk/validation_builder.rb +1 -0
  42. data/lib/easy_talk/version.rb +1 -1
  43. data/lib/easy_talk.rb +1 -0
  44. metadata +13 -4
@@ -23,7 +23,7 @@ module EasyTalk
23
23
  params(
24
24
  property_name: Symbol,
25
25
  schema: T::Hash[Symbol, T.untyped],
26
- options: T::Hash[Symbol, String],
26
+ options: T::Hash[Symbol, T.untyped],
27
27
  valid_options: T::Hash[Symbol, T.untyped]
28
28
  ).void
29
29
  end
@@ -59,6 +59,7 @@ module EasyTalk
59
59
  end
60
60
  end
61
61
 
62
+ sig { returns(T::Boolean) }
62
63
  def self.collection_type?
63
64
  false
64
65
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -14,7 +15,7 @@ module EasyTalk
14
15
  default: { type: T::Boolean, key: :default }
15
16
  }.freeze
16
17
 
17
- sig { params(name: Symbol, constraints: Hash).void }
18
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
18
19
  def initialize(name, constraints = {})
19
20
  super(name, { type: 'boolean' }, constraints, VALID_OPTIONS)
20
21
  end
@@ -1,9 +1,13 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  module EasyTalk
4
5
  module Builders
5
6
  # Base builder class for array-type properties.
6
7
  module CollectionHelpers
8
+ extend T::Sig
9
+
10
+ sig { returns(T::Boolean) }
7
11
  def collection_type?
8
12
  true
9
13
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'collection_helpers'
4
5
  require_relative '../ref_helper'
@@ -16,7 +17,7 @@ module EasyTalk
16
17
  'OneOfBuilder' => 'oneOf'
17
18
  }.freeze
18
19
 
19
- sig { params(name: Symbol, type: T.untyped, constraints: Hash).void }
20
+ sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
20
21
  # Initializes a new instance of the CompositionBuilder class.
21
22
  #
22
23
  # @param name [Symbol] The name of the composition.
@@ -32,7 +33,8 @@ module EasyTalk
32
33
 
33
34
  # Builds the composed JSON schema.
34
35
  #
35
- # @return [void]
36
+ # @return [Hash] The composed JSON schema.
37
+ sig { returns(T::Hash[Symbol, T.untyped]) }
36
38
  def build
37
39
  @context[@name.to_sym] = {
38
40
  type: 'object',
@@ -43,6 +45,7 @@ module EasyTalk
43
45
  # Returns the composer keyword based on the composer type.
44
46
  #
45
47
  # @return [String] The composer keyword.
48
+ sig { returns(T.nilable(String)) }
46
49
  def composer_keyword
47
50
  COMPOSER_TO_KEYWORD[@composer_type]
48
51
  end
@@ -50,6 +53,7 @@ module EasyTalk
50
53
  # Returns an array of schemas for the composed JSON schema.
51
54
  #
52
55
  # @return [Array<Hash>] The array of schemas.
56
+ sig { returns(T::Array[T.untyped]) }
53
57
  def schemas
54
58
  items.map do |type|
55
59
  if EasyTalk::RefHelper.should_use_ref?(type, @constraints)
@@ -66,6 +70,7 @@ module EasyTalk
66
70
  # Returns the items of the type.
67
71
  #
68
72
  # @return [T.untyped] The items of the type.
73
+ sig { returns(T.untyped) }
69
74
  def items
70
75
  @type.items
71
76
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -20,7 +21,7 @@ module EasyTalk
20
21
  }.freeze
21
22
 
22
23
  # Initializes a new instance of the IntegerBuilder class.
23
- sig { params(name: Symbol, constraints: Hash).void }
24
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
24
25
  def initialize(name, constraints = {})
25
26
  super(name, { type: 'integer' }, constraints, VALID_OPTIONS)
26
27
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -6,8 +7,10 @@ module EasyTalk
6
7
  module Builders
7
8
  # builder class for Null properties.
8
9
  class NullBuilder < BaseBuilder
10
+ extend T::Sig
11
+
9
12
  # Initializes a new instance of the NullBuilder class.
10
- sig { params(name: Symbol, _constraints: Hash).void }
13
+ sig { params(name: Symbol, _constraints: T::Hash[Symbol, T.untyped]).void }
11
14
  def initialize(name, _constraints = {})
12
15
  super(name, { type: 'null' }, {}, {})
13
16
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
 
@@ -6,6 +7,8 @@ module EasyTalk
6
7
  module Builders
7
8
  # Builder class for number properties.
8
9
  class NumberBuilder < BaseBuilder
10
+ extend T::Sig
11
+
9
12
  VALID_OPTIONS = {
10
13
  multiple_of: { type: T.any(Integer, Float), key: :multipleOf },
11
14
  minimum: { type: T.any(Integer, Float), key: :minimum },
@@ -18,7 +21,7 @@ module EasyTalk
18
21
  }.freeze
19
22
 
20
23
  # Initializes a new instance of the NumberBuilder class.
21
- sig { params(name: Symbol, constraints: Hash).void }
24
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
22
25
  def initialize(name, constraints = {})
23
26
  super(name, { type: 'number' }, constraints, VALID_OPTIONS)
24
27
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
  require_relative '../model_helper'
@@ -20,7 +21,12 @@ module EasyTalk
20
21
  # Required by BaseBuilder: recognized schema options for "object" types
21
22
  VALID_OPTIONS = {
22
23
  properties: { type: T::Hash[T.any(Symbol, String), T.untyped], key: :properties },
23
- additional_properties: { type: T::Boolean, key: :additionalProperties },
24
+ additional_properties: { type: T.any(T::Boolean, Class, T::Hash[Symbol, T.untyped]), key: :additionalProperties },
25
+ pattern_properties: { type: T::Hash[String, T.untyped], key: :patternProperties },
26
+ min_properties: { type: Integer, key: :minProperties },
27
+ max_properties: { type: Integer, key: :maxProperties },
28
+ dependencies: { type: T::Hash[String, T.any(T::Array[String], T::Hash[String, T.untyped])], key: :dependencies },
29
+ dependent_required: { type: T::Hash[String, T::Array[String]], key: :dependentRequired },
24
30
  subschemas: { type: T::Array[T.untyped], key: :subschemas },
25
31
  required: { type: T::Array[T.any(Symbol, String)], key: :required },
26
32
  defs: { type: T.untyped, key: :$defs },
@@ -55,6 +61,14 @@ module EasyTalk
55
61
  )
56
62
  end
57
63
 
64
+ # Override build to add additionalProperties after BaseBuilder validation
65
+ sig { override.returns(T::Hash[Symbol, T.untyped]) }
66
+ def build
67
+ result = super
68
+ process_additional_properties(result)
69
+ result
70
+ end
71
+
58
72
  private
59
73
 
60
74
  ##
@@ -99,8 +113,9 @@ module EasyTalk
99
113
  # Populate the final "required" array from @required_properties
100
114
  merged[:required] = @required_properties.to_a if @required_properties.any?
101
115
 
102
- # Add additionalProperties: false by default if not explicitly set
103
- merged[:additional_properties] = false unless merged.key?(:additional_properties)
116
+ # Process additionalProperties separately (don't let BaseBuilder validate it)
117
+ # Extract the value, process it, and we'll add it back after BaseBuilder runs
118
+ @additional_properties_value = merged.delete(:additional_properties)
104
119
 
105
120
  # Prune empty or nil values so we don't produce stuff like "properties": {} unnecessarily
106
121
  merged.reject! { |_k, v| v.nil? || v == {} || v == [] }
@@ -108,6 +123,52 @@ module EasyTalk
108
123
  merged
109
124
  end
110
125
 
126
+ ##
127
+ # Process additionalProperties to handle schema objects.
128
+ # Converts type classes or constraint hashes into proper JSON Schema.
129
+ # Called from build() method with the final schema hash.
130
+ #
131
+ def process_additional_properties(schema_hash)
132
+ value = @additional_properties_value
133
+
134
+ # If not set, use config default
135
+ if value.nil?
136
+ schema_hash[:additionalProperties] = EasyTalk.configuration.default_additional_properties
137
+ return
138
+ end
139
+
140
+ # Boolean: pass through as-is
141
+ if value.is_a?(TrueClass) || value.is_a?(FalseClass)
142
+ schema_hash[:additionalProperties] = value
143
+ return
144
+ end
145
+
146
+ # Class type: build schema
147
+ if value.is_a?(Class)
148
+ schema_hash[:additionalProperties] = build_additional_properties_schema(value, {})
149
+ return
150
+ end
151
+
152
+ # Hash with type + constraints: build schema with constraints
153
+ return unless value.is_a?(Hash)
154
+
155
+ type = value[:type] || value['type']
156
+ constraints = value.except(:type, 'type')
157
+ schema_hash[:additionalProperties] = build_additional_properties_schema(type, constraints)
158
+ end
159
+
160
+ ##
161
+ # Builds a JSON Schema for additionalProperties from a type and constraints.
162
+ # Uses the Property builder to generate the schema.
163
+ #
164
+ def build_additional_properties_schema(type, constraints)
165
+ return {} unless type
166
+
167
+ # Use Property builder to generate schema for the type
168
+ property = EasyTalk::Property.new(:_additional, type, constraints)
169
+ property.as_json
170
+ end
171
+
111
172
  ##
112
173
  # Given the property definitions hash, produce a new hash of
113
174
  # { property_name => [Property or nested schema builder result] }.
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  module EasyTalk
4
5
  module Builders
@@ -22,9 +23,12 @@ module EasyTalk
22
23
  #
23
24
  class Registry
24
25
  class << self
26
+ extend T::Sig
27
+
25
28
  # Get the hash of registered type builders.
26
29
  #
27
30
  # @return [Hash{String => Hash}] The registered builders with metadata
31
+ sig { returns(T::Hash[String, T::Hash[Symbol, T.untyped]]) }
28
32
  def registry
29
33
  @registry ||= {}
30
34
  end
@@ -43,6 +47,7 @@ module EasyTalk
43
47
  #
44
48
  # @example Register a collection type
45
49
  # Registry.register(CustomArray, CustomArrayBuilder, collection: true)
50
+ sig { params(type_key: T.any(T::Class[T.anything], String, Symbol), builder_class: T.untyped, collection: T::Boolean).void }
46
51
  def register(type_key, builder_class, collection: false)
47
52
  raise ArgumentError, 'Builder must respond to .new' unless builder_class.respond_to?(:new)
48
53
 
@@ -63,6 +68,7 @@ module EasyTalk
63
68
  # @example
64
69
  # builder_class, is_collection = Registry.resolve(String)
65
70
  # # => [StringBuilder, false]
71
+ sig { params(type: T.untyped).returns(T.nilable(T::Array[T.untyped])) }
66
72
  def resolve(type)
67
73
  entry = find_registration(type)
68
74
  return nil unless entry
@@ -74,6 +80,7 @@ module EasyTalk
74
80
  #
75
81
  # @param type_key [Class, String, Symbol] The type to check
76
82
  # @return [Boolean] true if the type is registered
83
+ sig { params(type_key: T.any(T::Class[T.anything], String, Symbol)).returns(T::Boolean) }
77
84
  def registered?(type_key)
78
85
  registry.key?(normalize_key(type_key))
79
86
  end
@@ -82,6 +89,7 @@ module EasyTalk
82
89
  #
83
90
  # @param type_key [Class, String, Symbol] The type to unregister
84
91
  # @return [Hash, nil] The removed registration or nil if not found
92
+ sig { params(type_key: T.any(T::Class[T.anything], String, Symbol)).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
85
93
  def unregister(type_key)
86
94
  registry.delete(normalize_key(type_key))
87
95
  end
@@ -89,6 +97,7 @@ module EasyTalk
89
97
  # Get a list of all registered type keys.
90
98
  #
91
99
  # @return [Array<String>] The registered type keys
100
+ sig { returns(T::Array[String]) }
92
101
  def registered_types
93
102
  registry.keys
94
103
  end
@@ -96,6 +105,7 @@ module EasyTalk
96
105
  # Reset the registry to empty state and re-register built-in types.
97
106
  #
98
107
  # @return [void]
108
+ sig { void }
99
109
  def reset!
100
110
  @registry = nil
101
111
  register_built_in_types
@@ -105,6 +115,7 @@ module EasyTalk
105
115
  # This is called during gem initialization and after reset!
106
116
  #
107
117
  # @return [void]
118
+ sig { void }
108
119
  def register_built_in_types
109
120
  register(String, Builders::StringBuilder)
110
121
  register(Integer, Builders::IntegerBuilder)
@@ -116,9 +127,10 @@ module EasyTalk
116
127
  register(Date, Builders::TemporalBuilder::DateBuilder)
117
128
  register(DateTime, Builders::TemporalBuilder::DatetimeBuilder)
118
129
  register(Time, Builders::TemporalBuilder::TimeBuilder)
119
- register('anyOf', Builders::CompositionBuilder::AnyOfBuilder, collection: true)
120
130
  register('allOf', Builders::CompositionBuilder::AllOfBuilder, collection: true)
131
+ register('anyOf', Builders::CompositionBuilder::AnyOfBuilder, collection: true)
121
132
  register('oneOf', Builders::CompositionBuilder::OneOfBuilder, collection: true)
133
+ register('EasyTalk::Types::Tuple', Builders::TupleBuilder, collection: true)
122
134
  register('T::Types::TypedArray', Builders::TypedArrayBuilder, collection: true)
123
135
  register('T::Types::Union', Builders::UnionBuilder, collection: true)
124
136
  end
@@ -129,6 +141,7 @@ module EasyTalk
129
141
  #
130
142
  # @param type_key [Class, String, Symbol] The type key to normalize
131
143
  # @return [String] The normalized key
144
+ sig { params(type_key: T.any(T::Class[T.anything], String, Symbol)).returns(String) }
132
145
  def normalize_key(type_key)
133
146
  case type_key
134
147
  when Class
@@ -146,6 +159,7 @@ module EasyTalk
146
159
  #
147
160
  # @param type [Object] The type to find
148
161
  # @return [Hash, nil] The registration entry or nil
162
+ sig { params(type: T.untyped).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
149
163
  def find_registration(type)
150
164
  # Strategy 1: Check type.class.name (for Sorbet types like T::Types::TypedArray)
151
165
  class_name = type.class.name.to_s
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'base_builder'
4
5
  require 'js_regex' # Compile the ruby regex to JS regex
@@ -20,11 +21,12 @@ module EasyTalk
20
21
  default: { type: String, key: :default }
21
22
  }.freeze
22
23
 
23
- sig { params(name: Symbol, constraints: Hash).void }
24
+ sig { params(name: Symbol, constraints: T::Hash[Symbol, T.untyped]).void }
24
25
  def initialize(name, constraints = {})
25
26
  super(name, { type: 'string' }, constraints, VALID_OPTIONS)
26
27
  end
27
28
 
29
+ sig { returns(T::Hash[Symbol, T.untyped]) }
28
30
  def build
29
31
  super.tap do |schema|
30
32
  pattern = schema[:pattern]
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'string_builder'
4
5
 
@@ -6,11 +7,14 @@ module EasyTalk
6
7
  module Builders
7
8
  # Builder class for temporal properties (date, datetime, time).
8
9
  class TemporalBuilder < StringBuilder
10
+ extend T::Sig
11
+
9
12
  # Initializes a new instance of the TemporalBuilder class.
10
13
  #
11
14
  # @param property_name [Symbol] The name of the property.
12
15
  # @param options [Hash] The options for the builder.
13
16
  # @param format [String] The format of the temporal property (date, date-time, time).
17
+ sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped], format: T.nilable(String)).void }
14
18
  def initialize(property_name, options = {}, format = nil)
15
19
  super(property_name, options)
16
20
  @format = format
@@ -26,6 +30,7 @@ module EasyTalk
26
30
 
27
31
  # Builder class for date properties.
28
32
  class DateBuilder < TemporalBuilder
33
+ sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped]).void }
29
34
  def initialize(property_name, options = {})
30
35
  super(property_name, options, 'date')
31
36
  end
@@ -33,6 +38,7 @@ module EasyTalk
33
38
 
34
39
  # Builder class for datetime properties.
35
40
  class DatetimeBuilder < TemporalBuilder
41
+ sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped]).void }
36
42
  def initialize(property_name, options = {})
37
43
  super(property_name, options, 'date-time')
38
44
  end
@@ -40,6 +46,7 @@ module EasyTalk
40
46
 
41
47
  # Builder class for time properties.
42
48
  class TimeBuilder < TemporalBuilder
49
+ sig { params(property_name: Symbol, options: T::Hash[Symbol, T.untyped]).void }
43
50
  def initialize(property_name, options = {})
44
51
  super(property_name, options, 'time')
45
52
  end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module EasyTalk
5
+ module Builders
6
+ # Builder class for tuple array properties (T::Tuple[Type1, Type2, ...]).
7
+ #
8
+ # Tuples are arrays with positional type validation where each index
9
+ # has a specific expected type.
10
+ #
11
+ # @example Basic tuple
12
+ # property :coordinates, T::Tuple[Float, Float]
13
+ #
14
+ # @example Tuple with additional items constraint
15
+ # property :record, T::Tuple[String, Integer], additional_items: false
16
+ #
17
+ class TupleBuilder < BaseBuilder
18
+ extend T::Sig
19
+
20
+ # NOTE: additional_items is handled separately in build() since it can be a type
21
+ VALID_OPTIONS = {
22
+ min_items: { type: Integer, key: :minItems },
23
+ max_items: { type: Integer, key: :maxItems },
24
+ unique_items: { type: T::Boolean, key: :uniqueItems }
25
+ }.freeze
26
+
27
+ attr_reader :type
28
+
29
+ sig { params(name: Symbol, type: Types::Tuple, constraints: T::Hash[Symbol, T.untyped]).void }
30
+ def initialize(name, type, constraints = {})
31
+ @name = name
32
+ @type = type
33
+ # Work on a copy to avoid mutating the original constraints hash
34
+ local_constraints = constraints.dup
35
+ # Extract additional_items before passing to super (it's handled separately in build)
36
+ @additional_items_constraint = local_constraints.delete(:additional_items)
37
+ super(name, { type: 'array' }, local_constraints, VALID_OPTIONS)
38
+ end
39
+
40
+ sig { returns(T::Boolean) }
41
+ def self.collection_type?
42
+ true
43
+ end
44
+
45
+ # Builds the tuple schema with positional items.
46
+ #
47
+ # @return [Hash] The built schema.
48
+ sig { returns(T::Hash[Symbol, T.untyped]) }
49
+ def build
50
+ schema = super
51
+
52
+ # Build items array from tuple types
53
+ schema[:items] = build_items
54
+
55
+ # Handle additional_items constraint
56
+ schema[:additionalItems] = build_additional_items_schema(@additional_items_constraint) unless @additional_items_constraint.nil?
57
+
58
+ schema
59
+ end
60
+
61
+ private
62
+
63
+ # Builds the items array from tuple types.
64
+ #
65
+ # @return [Array<Hash>] Array of schemas for each position
66
+ sig { returns(T::Array[T::Hash[Symbol, T.untyped]]) }
67
+ def build_items
68
+ type.types.map.with_index do |item_type, index|
69
+ Property.new(:"#{@name}_item_#{index}", item_type, {}).build
70
+ end
71
+ end
72
+
73
+ # Builds the additionalItems schema value.
74
+ #
75
+ # @param value [Boolean, Class] The additional_items constraint
76
+ # @return [Boolean, Hash] The schema value for additionalItems
77
+ sig { params(value: T.untyped).returns(T.any(T::Boolean, T::Hash[Symbol, T.untyped])) }
78
+ def build_additional_items_schema(value)
79
+ case value
80
+ when true, false
81
+ value
82
+ else
83
+ # It's a type - build a schema for it
84
+ Property.new(:"#{@name}_additional", value, {}).build
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -1,10 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'collection_helpers'
4
5
 
5
6
  module EasyTalk
6
7
  module Builders
7
- # Builder class for array properties.
8
+ # Builder class for homogeneous array properties (T::Array[Type]).
8
9
  class TypedArrayBuilder < BaseBuilder
9
10
  extend CollectionHelpers
10
11
  extend T::Sig
@@ -20,7 +21,7 @@ module EasyTalk
20
21
 
21
22
  attr_reader :type
22
23
 
23
- sig { params(name: Symbol, type: T.untyped, constraints: Hash).void }
24
+ sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
24
25
  def initialize(name, type, constraints = {})
25
26
  @name = name
26
27
  @type = type
@@ -49,6 +50,7 @@ module EasyTalk
49
50
  end
50
51
  end
51
52
 
53
+ sig { returns(T.untyped) }
52
54
  def inner_type
53
55
  return unless type.is_a?(T::Types::TypedArray)
54
56
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'collection_helpers'
4
5
 
@@ -9,7 +10,7 @@ module EasyTalk
9
10
  extend CollectionHelpers
10
11
  extend T::Sig
11
12
 
12
- sig { params(name: Symbol, type: T.untyped, constraints: T.untyped).void }
13
+ sig { params(name: Symbol, type: T.untyped, constraints: T::Hash[Symbol, T.untyped]).void }
13
14
  def initialize(name, type, constraints)
14
15
  @name = name
15
16
  @type = type
@@ -17,18 +18,21 @@ module EasyTalk
17
18
  @context = {}
18
19
  end
19
20
 
21
+ sig { returns(T::Hash[Symbol, T.untyped]) }
20
22
  def build
21
23
  @context[@name] = {
22
24
  'anyOf' => schemas
23
25
  }
24
26
  end
25
27
 
28
+ sig { returns(T::Array[T.untyped]) }
26
29
  def schemas
27
30
  types.map do |type|
28
31
  Property.new(@name, type, @constraints).build
29
32
  end
30
33
  end
31
34
 
35
+ sig { returns(T.untyped) }
32
36
  def types
33
37
  @type.types
34
38
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  require_relative 'naming_strategies'
4
5
 
5
6
  module EasyTalk
6
7
  class Configuration
8
+ extend T::Sig
9
+
7
10
  # JSON Schema draft version URIs
8
11
  SCHEMA_VERSIONS = {
9
12
  draft202012: 'https://json-schema.org/draft/2020-12/schema',
@@ -14,9 +17,11 @@ module EasyTalk
14
17
  }.freeze
15
18
 
16
19
  attr_accessor :default_additional_properties, :nilable_is_optional, :auto_validations, :schema_version, :schema_id,
17
- :use_refs, :validation_adapter, :default_error_format, :error_type_base_uri, :include_error_codes
20
+ :use_refs, :validation_adapter, :default_error_format, :error_type_base_uri, :include_error_codes,
21
+ :base_schema_uri, :auto_generate_ids, :prefer_external_refs
18
22
  attr_reader :property_naming_strategy
19
23
 
24
+ sig { void }
20
25
  def initialize
21
26
  @default_additional_properties = false
22
27
  @nilable_is_optional = false
@@ -28,16 +33,21 @@ module EasyTalk
28
33
  @default_error_format = :flat
29
34
  @error_type_base_uri = 'about:blank'
30
35
  @include_error_codes = true
36
+ @base_schema_uri = nil
37
+ @auto_generate_ids = false
38
+ @prefer_external_refs = false
31
39
  self.property_naming_strategy = :identity
32
40
  end
33
41
 
34
42
  # Returns the URI for the configured schema version, or nil if :none
43
+ sig { returns(T.nilable(String)) }
35
44
  def schema_uri
36
45
  return nil if @schema_version == :none
37
46
 
38
47
  SCHEMA_VERSIONS[@schema_version] || @schema_version.to_s
39
48
  end
40
49
 
50
+ sig { params(strategy: T.any(Symbol, T.proc.params(arg0: T.untyped).returns(Symbol))).void }
41
51
  def property_naming_strategy=(strategy)
42
52
  @property_naming_strategy = EasyTalk::NamingStrategies.derive_strategy(strategy)
43
53
  end
@@ -56,17 +66,22 @@ module EasyTalk
56
66
  # EasyTalk.configure do |config|
57
67
  # config.register_type Money, MoneySchemaBuilder
58
68
  # end
69
+ sig { params(type_key: T.any(T::Class[T.anything], String, Symbol), builder_class: T.untyped, collection: T::Boolean).void }
59
70
  def register_type(type_key, builder_class, collection: false)
60
71
  EasyTalk::Builders::Registry.register(type_key, builder_class, collection: collection)
61
72
  end
62
73
  end
63
74
 
64
75
  class << self
76
+ extend T::Sig
77
+
78
+ sig { returns(Configuration) }
65
79
  def configuration
66
80
  @configuration ||= Configuration.new
67
81
  end
68
82
 
69
- def configure
83
+ sig { params(block: T.proc.params(config: Configuration).void).void }
84
+ def configure(&block)
70
85
  yield(configuration)
71
86
  end
72
87
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  module EasyTalk
4
5
  class Error < StandardError; end
@@ -1,8 +1,11 @@
1
1
  # frozen_string_literal: true
2
+ # typed: true
2
3
 
3
4
  module EasyTalk
4
5
  # Helper module for generating consistent error messages
5
6
  module ErrorHelper
7
+ extend T::Sig
8
+
6
9
  def self.raise_constraint_error(property_name:, constraint_name:, expected:, got:)
7
10
  message = "Error in property '#{property_name}': Constraint '#{constraint_name}' expects #{expected}, " \
8
11
  "but received #{got.inspect} (#{got.class})."