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,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
|
|
@@ -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
|