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.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -39
  3. data/.yardopts +13 -0
  4. data/CHANGELOG.md +164 -0
  5. data/README.md +442 -1529
  6. data/Rakefile +27 -0
  7. data/docs/.gitignore +1 -0
  8. data/docs/about.markdown +28 -8
  9. data/docs/getting-started.markdown +102 -0
  10. data/docs/index.markdown +51 -4
  11. data/docs/json_schema_compliance.md +169 -0
  12. data/docs/nested-models.markdown +216 -0
  13. data/docs/primitive-schema-rfc.md +894 -0
  14. data/docs/property-types.markdown +212 -0
  15. data/docs/schema-definition.markdown +180 -0
  16. data/lib/easy_talk/builders/base_builder.rb +6 -3
  17. data/lib/easy_talk/builders/boolean_builder.rb +2 -1
  18. data/lib/easy_talk/builders/collection_helpers.rb +4 -0
  19. data/lib/easy_talk/builders/composition_builder.rb +16 -13
  20. data/lib/easy_talk/builders/integer_builder.rb +2 -1
  21. data/lib/easy_talk/builders/null_builder.rb +4 -1
  22. data/lib/easy_talk/builders/number_builder.rb +4 -1
  23. data/lib/easy_talk/builders/object_builder.rb +109 -33
  24. data/lib/easy_talk/builders/registry.rb +182 -0
  25. data/lib/easy_talk/builders/string_builder.rb +3 -1
  26. data/lib/easy_talk/builders/temporal_builder.rb +7 -0
  27. data/lib/easy_talk/builders/tuple_builder.rb +89 -0
  28. data/lib/easy_talk/builders/typed_array_builder.rb +19 -6
  29. data/lib/easy_talk/builders/union_builder.rb +5 -1
  30. data/lib/easy_talk/configuration.rb +47 -2
  31. data/lib/easy_talk/error_formatter/base.rb +100 -0
  32. data/lib/easy_talk/error_formatter/error_code_mapper.rb +82 -0
  33. data/lib/easy_talk/error_formatter/flat.rb +38 -0
  34. data/lib/easy_talk/error_formatter/json_pointer.rb +38 -0
  35. data/lib/easy_talk/error_formatter/jsonapi.rb +64 -0
  36. data/lib/easy_talk/error_formatter/path_converter.rb +53 -0
  37. data/lib/easy_talk/error_formatter/rfc7807.rb +69 -0
  38. data/lib/easy_talk/error_formatter.rb +143 -0
  39. data/lib/easy_talk/errors.rb +3 -0
  40. data/lib/easy_talk/errors_helper.rb +66 -34
  41. data/lib/easy_talk/json_schema_equality.rb +46 -0
  42. data/lib/easy_talk/keywords.rb +0 -1
  43. data/lib/easy_talk/model.rb +148 -89
  44. data/lib/easy_talk/model_helper.rb +17 -0
  45. data/lib/easy_talk/naming_strategies.rb +24 -0
  46. data/lib/easy_talk/property.rb +23 -94
  47. data/lib/easy_talk/ref_helper.rb +33 -0
  48. data/lib/easy_talk/schema.rb +199 -0
  49. data/lib/easy_talk/schema_definition.rb +57 -5
  50. data/lib/easy_talk/schema_methods.rb +111 -0
  51. data/lib/easy_talk/sorbet_extension.rb +1 -0
  52. data/lib/easy_talk/tools/function_builder.rb +1 -1
  53. data/lib/easy_talk/type_introspection.rb +222 -0
  54. data/lib/easy_talk/types/base_composer.rb +2 -1
  55. data/lib/easy_talk/types/composer.rb +4 -0
  56. data/lib/easy_talk/types/tuple.rb +77 -0
  57. data/lib/easy_talk/validation_adapters/active_model_adapter.rb +617 -0
  58. data/lib/easy_talk/validation_adapters/active_model_schema_validation.rb +106 -0
  59. data/lib/easy_talk/validation_adapters/base.rb +156 -0
  60. data/lib/easy_talk/validation_adapters/none_adapter.rb +45 -0
  61. data/lib/easy_talk/validation_adapters/registry.rb +87 -0
  62. data/lib/easy_talk/validation_builder.rb +29 -309
  63. data/lib/easy_talk/version.rb +1 -1
  64. data/lib/easy_talk.rb +42 -0
  65. metadata +38 -7
  66. data/docs/404.html +0 -25
  67. data/docs/_posts/2024-05-07-welcome-to-jekyll.markdown +0 -29
  68. data/easy_talk.gemspec +0 -39
@@ -0,0 +1,222 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ require 'bigdecimal'
5
+
6
+ module EasyTalk
7
+ # Centralized module for robust type introspection.
8
+ #
9
+ # This module provides predicate methods for detecting types without relying
10
+ # on brittle string-based checks. It uses Sorbet's type system properly and
11
+ # handles edge cases gracefully.
12
+ #
13
+ # @example Checking if a type is boolean
14
+ # TypeIntrospection.boolean_type?(T::Boolean) # => true
15
+ # TypeIntrospection.boolean_type?(TrueClass) # => true
16
+ # TypeIntrospection.boolean_type?(String) # => false
17
+ #
18
+ # @example Getting JSON Schema type
19
+ # TypeIntrospection.json_schema_type(Integer) # => 'integer'
20
+ # TypeIntrospection.json_schema_type(Float) # => 'number'
21
+ #
22
+ module TypeIntrospection
23
+ # Mapping of Ruby classes to JSON Schema types
24
+ PRIMITIVE_TO_JSON_SCHEMA = {
25
+ String => 'string',
26
+ Integer => 'integer',
27
+ Float => 'number',
28
+ BigDecimal => 'number',
29
+ TrueClass => 'boolean',
30
+ FalseClass => 'boolean',
31
+ NilClass => 'null'
32
+ }.freeze
33
+
34
+ class << self
35
+ extend T::Sig
36
+
37
+ # Check if type represents a boolean (T::Boolean or TrueClass/FalseClass).
38
+ #
39
+ # @param type [Object] The type to check
40
+ # @return [Boolean] true if the type is a boolean type
41
+ #
42
+ # @example
43
+ # boolean_type?(T::Boolean) # => true
44
+ # boolean_type?(TrueClass) # => true
45
+ # boolean_type?(FalseClass) # => true
46
+ # boolean_type?(String) # => false
47
+ sig { params(type: T.untyped).returns(T::Boolean) }
48
+ def boolean_type?(type)
49
+ return false if type.nil?
50
+ return true if [TrueClass, FalseClass].include?(type)
51
+ return true if type.respond_to?(:raw_type) && [TrueClass, FalseClass].include?(type.raw_type)
52
+
53
+ # Check for T::Boolean which is a TypeAlias with name 'T::Boolean'
54
+ return true if type.respond_to?(:name) && type.name == 'T::Boolean'
55
+
56
+ # Check for union types containing TrueClass and FalseClass
57
+ if type.respond_to?(:types)
58
+ type_classes = type.types.map { |t| t.respond_to?(:raw_type) ? t.raw_type : t }
59
+ return type_classes.sort_by(&:name) == [FalseClass, TrueClass].sort_by(&:name)
60
+ end
61
+
62
+ false
63
+ end
64
+
65
+ # Check if a resolved type class represents a boolean union ([TrueClass, FalseClass]).
66
+ #
67
+ # This is useful when checking resolved type classes rather than raw Sorbet types.
68
+ # The internal representation of T::Boolean resolves to [TrueClass, FalseClass].
69
+ #
70
+ # @param type_class [Object] The resolved type class to check
71
+ # @return [Boolean] true if the type class is a boolean union array
72
+ #
73
+ # @example
74
+ # boolean_union_type?([TrueClass, FalseClass]) # => true
75
+ # boolean_union_type?(TrueClass) # => false
76
+ # boolean_union_type?(String) # => false
77
+ sig { params(type_class: T.untyped).returns(T::Boolean) }
78
+ def boolean_union_type?(type_class)
79
+ type_class.is_a?(Array) && type_class.sort_by(&:name) == [FalseClass, TrueClass].sort_by(&:name)
80
+ end
81
+
82
+ # Check if type is a typed array (T::Array[...]).
83
+ #
84
+ # @param type [Object] The type to check
85
+ # @return [Boolean] true if the type is a typed array
86
+ #
87
+ # @example
88
+ # typed_array?(T::Array[String]) # => true
89
+ # typed_array?(Array) # => false
90
+ sig { params(type: T.untyped).returns(T::Boolean) }
91
+ def typed_array?(type)
92
+ return false if type.nil?
93
+
94
+ type.is_a?(T::Types::TypedArray)
95
+ end
96
+
97
+ # Check if type is any array type (plain Array, T::Array[...], or T::Tuple[...]).
98
+ #
99
+ # @param type [Object] The type to check
100
+ # @return [Boolean] true if the type is an array type
101
+ #
102
+ # @example
103
+ # array_type?(Array) # => true
104
+ # array_type?(T::Array[String]) # => true
105
+ # array_type?(T::Tuple[String, Integer]) # => true
106
+ # array_type?(String) # => false
107
+ sig { params(type: T.untyped).returns(T::Boolean) }
108
+ def array_type?(type)
109
+ return false if type.nil?
110
+
111
+ type == Array || type.is_a?(T::Types::TypedArray) || type.is_a?(EasyTalk::Types::Tuple)
112
+ end
113
+
114
+ # Check if type is nilable (T.nilable(...)).
115
+ #
116
+ # @param type [Object] The type to check
117
+ # @return [Boolean] true if the type is nilable
118
+ #
119
+ # @example
120
+ # nilable_type?(T.nilable(String)) # => true
121
+ # nilable_type?(String) # => false
122
+ sig { params(type: T.untyped).returns(T::Boolean) }
123
+ def nilable_type?(type)
124
+ return false if type.nil?
125
+
126
+ type.respond_to?(:nilable?) && type.nilable?
127
+ end
128
+
129
+ # Check if type is a primitive Ruby type.
130
+ #
131
+ # @param type [Object] The type to check
132
+ # @return [Boolean] true if the type is a primitive
133
+ sig { params(type: T.untyped).returns(T::Boolean) }
134
+ def primitive_type?(type)
135
+ return false if type.nil?
136
+
137
+ resolved = if type.is_a?(Class)
138
+ type
139
+ elsif type.respond_to?(:raw_type)
140
+ type.raw_type
141
+ end
142
+ PRIMITIVE_TO_JSON_SCHEMA.key?(resolved)
143
+ end
144
+
145
+ # Get JSON Schema type string for a Ruby type.
146
+ #
147
+ # @param type [Object] The type to convert
148
+ # @return [String] The JSON Schema type string
149
+ #
150
+ # @example
151
+ # json_schema_type(Integer) # => 'integer'
152
+ # json_schema_type(Float) # => 'number'
153
+ # json_schema_type(BigDecimal) # => 'number'
154
+ # json_schema_type(String) # => 'string'
155
+ sig { params(type: T.untyped).returns(String) }
156
+ def json_schema_type(type)
157
+ return 'object' if type.nil?
158
+ return 'boolean' if boolean_type?(type)
159
+
160
+ resolved_class = if type.is_a?(Class)
161
+ type
162
+ elsif type.respond_to?(:raw_type)
163
+ type.raw_type
164
+ end
165
+
166
+ PRIMITIVE_TO_JSON_SCHEMA[resolved_class] || resolved_class&.name&.downcase || 'object'
167
+ end
168
+
169
+ # Get the Ruby class for a type, handling Sorbet types.
170
+ #
171
+ # @param type [Object] The type to resolve
172
+ # @return [Class, Array<Class>, nil] The resolved class or classes
173
+ #
174
+ # @example
175
+ # get_type_class(String) # => String
176
+ # get_type_class(T::Boolean) # => [TrueClass, FalseClass]
177
+ # get_type_class(T::Array[String]) # => Array
178
+ sig { params(type: T.untyped).returns(T.untyped) }
179
+ def get_type_class(type)
180
+ return nil if type.nil?
181
+ return type if type.is_a?(Class)
182
+ return type.raw_type if type.respond_to?(:raw_type)
183
+ return Array if type.is_a?(T::Types::TypedArray) || type.is_a?(EasyTalk::Types::Tuple)
184
+ return [TrueClass, FalseClass] if boolean_type?(type)
185
+
186
+ if nilable_type?(type)
187
+ inner = extract_inner_type(type)
188
+ return get_type_class(inner) if inner && inner != type
189
+ end
190
+
191
+ nil
192
+ end
193
+
194
+ # Extract inner type from nilable or complex types.
195
+ #
196
+ # @param type [Object] The type to unwrap
197
+ # @return [Object] The inner type, or the original type if not wrapped
198
+ #
199
+ # @example
200
+ # extract_inner_type(T.nilable(String)) # => String
201
+ sig { params(type: T.untyped).returns(T.untyped) }
202
+ def extract_inner_type(type)
203
+ return type if type.nil?
204
+
205
+ if type.respond_to?(:unwrap_nilable)
206
+ unwrapped = type.unwrap_nilable
207
+ return unwrapped.respond_to?(:raw_type) ? unwrapped.raw_type : unwrapped
208
+ end
209
+
210
+ if type.respond_to?(:types)
211
+ non_nil = type.types.find do |t|
212
+ raw = t.respond_to?(:raw_type) ? t.raw_type : t
213
+ raw != NilClass
214
+ end
215
+ return non_nil.respond_to?(:raw_type) ? non_nil.raw_type : non_nil if non_nil
216
+ end
217
+
218
+ type
219
+ end
220
+ end
221
+ end
222
+ end
@@ -3,7 +3,7 @@
3
3
  module EasyTalk
4
4
  module Types
5
5
  # no-doc
6
- class BaseComposer
6
+ class BaseComposer < T::Types::Base
7
7
  extend T::Sig
8
8
  extend T::Generic
9
9
 
@@ -16,6 +16,7 @@ module EasyTalk
16
16
  #
17
17
  # @param args [Array] the items to be assigned to the instance variable @items
18
18
  def initialize(*args)
19
+ super()
19
20
  @items = args
20
21
  end
21
22
  end
@@ -16,6 +16,10 @@ module EasyTalk
16
16
  self.class.name
17
17
  end
18
18
 
19
+ def to_s
20
+ name.to_s
21
+ end
22
+
19
23
  # Represents a composition type that allows all of the specified types.
20
24
  class AllOf < Composer
21
25
  def self.name
@@ -0,0 +1,77 @@
1
+ # frozen_string_literal: true
2
+
3
+ module EasyTalk
4
+ module Types
5
+ # Represents a tuple type for arrays with positional type validation.
6
+ #
7
+ # A tuple is an array where each position has a specific type. This class
8
+ # stores the types for each position.
9
+ #
10
+ # @example Basic tuple
11
+ # T::Tuple[String, Integer] # First item must be String, second must be Integer
12
+ #
13
+ # @example With additional_items constraint
14
+ # property :flags, T::Tuple[T::Boolean, T::Boolean], additional_items: false
15
+ #
16
+ class Tuple
17
+ extend T::Sig
18
+
19
+ # @return [Array<Object>] The types for each position in the tuple
20
+ sig { returns(T::Array[T.untyped]) }
21
+ attr_reader :types
22
+
23
+ # Creates a new Tuple instance with the given positional types.
24
+ #
25
+ # @param types [Array] The types for each position in the tuple
26
+ # @raise [ArgumentError] if types is empty or contains nil values
27
+ sig { params(types: T.untyped).void }
28
+ def initialize(*types)
29
+ raise ArgumentError, 'Tuple requires at least one type' if types.empty?
30
+ raise ArgumentError, 'Tuple types cannot be nil' if types.any?(&:nil?)
31
+
32
+ @types = types.freeze
33
+ end
34
+
35
+ # Returns a string representation of the tuple type.
36
+ #
37
+ # @return [String] A human-readable representation
38
+ sig { returns(String) }
39
+ def to_s
40
+ type_names = @types.map { |t| (t.respond_to?(:name) && t.name) || t.to_s }
41
+ "T::Tuple[#{type_names.join(', ')}]"
42
+ end
43
+
44
+ # Returns the name of this type (used by Property for error messages).
45
+ #
46
+ # @return [String] The type name
47
+ sig { returns(String) }
48
+ def name
49
+ to_s
50
+ end
51
+ end
52
+ end
53
+ end
54
+
55
+ # Add T::Tuple module for bracket syntax
56
+ module T
57
+ # Provides tuple type syntax: T::Tuple[Type1, Type2, ...]
58
+ #
59
+ # Creates a tuple type that validates array elements by position.
60
+ #
61
+ # @example Basic usage
62
+ # property :coordinates, T::Tuple[Float, Float]
63
+ # property :record, T::Tuple[String, Integer, T::Boolean]
64
+ #
65
+ # @example With additional_items constraint
66
+ # property :flags, T::Tuple[T::Boolean, T::Boolean], additional_items: false
67
+ #
68
+ module Tuple
69
+ # Creates a new Tuple type with the given positional types.
70
+ #
71
+ # @param types [Array] The types for each position
72
+ # @return [EasyTalk::Types::Tuple] A new Tuple instance
73
+ def self.[](*types)
74
+ EasyTalk::Types::Tuple.new(*types)
75
+ end
76
+ end
77
+ end