easy_talk 0.2.2 → 1.0.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/CHANGELOG.md +15 -0
- data/README.md +118 -117
- data/lib/easy_talk/builders/object_builder.rb +152 -56
- data/lib/easy_talk/model.rb +1 -38
- data/lib/easy_talk/schema_definition.rb +17 -3
- data/lib/easy_talk/tools/function_builder.rb +27 -15
- data/lib/easy_talk/version.rb +1 -1
- metadata +23 -41
- data/lib/easy_talk/schema_errors_mapper.rb +0 -23
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa39fe0359df9334a186807b3e67679b752806db59eb9b03829ec875c6382818
|
4
|
+
data.tar.gz: 150814754a0604fc0149bf042c73d798fe042935ffde11b52c2254ac765c05e8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ea9d64a999260983afac690850ae5095b4e2d00583feb1a6dd4baa0a0cb377a82566dae1245e1b767e5ff79549f28e60209b43fa3d765a8b51c46ee6969425bd
|
7
|
+
data.tar.gz: 20e3bea29ad389126937924f431f8729be43b172f8f868ae5fc0189d729e1d19642a9fa0e74a34322e1acafd2b74c6b2fad20bb8f081c7ea504364a2daaf3d99
|
data/CHANGELOG.md
CHANGED
@@ -1,3 +1,18 @@
|
|
1
|
+
## [1.0.0] - 2024-06-01
|
2
|
+
- Use `Hash` instead of `:object` for inline object schema definition.
|
3
|
+
example:
|
4
|
+
```ruby
|
5
|
+
property :email, Hash do
|
6
|
+
property :address, :string
|
7
|
+
property :verified, :boolean
|
8
|
+
end
|
9
|
+
```
|
10
|
+
- Loosen up the gemspec version requirement. Makes it flexible to use the library with future versions of Rails (i.e 8.*).
|
11
|
+
- Removed JSONScheemer gem dependency.
|
12
|
+
- The library does not validate by default anymore. Validating an instance requires that you explicitly define ActiveModel validations in your EasyTalk model. See: https://github.com/sergiobayona/easy_talk/blob/main/spec/easy_talk/activemodel_integration_spec.rb.
|
13
|
+
- Internal improvements to `EasyTalk::ObjectBuilder` class. No changes to the public API.
|
14
|
+
- Expanded the test suite.
|
15
|
+
|
1
16
|
## [0.2.2] - 2024-05-17
|
2
17
|
- Fixed a bug where optional properties were not excluded from the required list.
|
3
18
|
|
data/README.md
CHANGED
@@ -4,10 +4,9 @@ EasyTalk is a Ruby library that simplifies defining and generating JSON Schema d
|
|
4
4
|
|
5
5
|
Key Features
|
6
6
|
* Intuitive Schema Definition: Use Ruby classes and methods to define JSON Schema documents easily.
|
7
|
-
*
|
8
|
-
*
|
9
|
-
* Validation:
|
10
|
-
* Integration with ActiveModel: EasyTalk integrates with ActiveModel to provide additional functionality such as attribute assignment, introspections, validations, translation (i18n), and more.
|
7
|
+
* LLM Function Support: Ideal for integrating with Large Language Models (LLMs) such as OpenAI’s GPT series. EasyTalk enables you to effortlessly create JSON Schema documents describing the inputs and outputs of LLM function calls.
|
8
|
+
* Schema Composition: Define EasyTalk models and reference them in other EasyTalk models to create complex schemas.
|
9
|
+
* Validation: Write validations using ActiveModel’s validations.
|
11
10
|
|
12
11
|
Inspiration
|
13
12
|
Inspired by Python's Pydantic library, EasyTalk brings similar functionality to the Ruby ecosystem, providing a Ruby-friendly approach to JSON Schema operations.
|
@@ -18,85 +17,78 @@ Example Use:
|
|
18
17
|
class User
|
19
18
|
include EasyTalk::Model
|
20
19
|
|
20
|
+
validates :name, :email, :group, presence: true
|
21
|
+
validates :age, numericality: { greater_than_or_equal_to: 18, less_than_or_equal_to: 100 }
|
22
|
+
|
21
23
|
define_schema do
|
22
24
|
title "User"
|
23
25
|
description "A user of the system"
|
24
26
|
property :name, String, description: "The user's name", title: "Full Name"
|
25
|
-
property :email,
|
27
|
+
property :email, Hash do
|
26
28
|
property :address, String, format: "email", description: "The user's email", title: "Email Address"
|
27
29
|
property :verified, T::Boolean, description: "Whether the email is verified"
|
28
30
|
end
|
29
|
-
property :group,
|
31
|
+
property :group, Integer, enum: [1, 2, 3], default: 1, description: "The user's group"
|
30
32
|
property :age, Integer, minimum: 18, maximum: 100, description: "The user's age"
|
31
|
-
property :tags, T::Array[String], min_items: 1,
|
33
|
+
property :tags, T::Array[String], min_items: 1, unique_items: true, description: "The user's tags"
|
32
34
|
end
|
33
35
|
end
|
34
36
|
```
|
35
37
|
|
36
|
-
Calling `User.json_schema` will return the JSON Schema for the User class:
|
38
|
+
Calling `User.json_schema` will return the Ruby representation of the JSON Schema for the `User` class:
|
37
39
|
|
38
|
-
```
|
40
|
+
```ruby
|
39
41
|
{
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
"address": {
|
53
|
-
"title": "Email Address",
|
54
|
-
"description": "The user's email",
|
55
|
-
"type": "string",
|
56
|
-
"format": "email"
|
57
|
-
},
|
58
|
-
"verified": {
|
59
|
-
"type": "boolean",
|
60
|
-
"description": "Whether the email is verified"
|
61
|
-
}
|
62
|
-
},
|
63
|
-
"required": [
|
64
|
-
"address",
|
65
|
-
"verified"
|
66
|
-
]
|
67
|
-
},
|
68
|
-
"group": {
|
69
|
-
"type": "number",
|
70
|
-
"enum": [1, 2, 3],
|
71
|
-
"default": 1,
|
72
|
-
"description": "The user's group"
|
73
|
-
},
|
74
|
-
"age": {
|
75
|
-
"type": "integer",
|
76
|
-
"minimum": 18,
|
77
|
-
"maximum": 100,
|
78
|
-
"description": "The user's age"
|
42
|
+
"type" => "object",
|
43
|
+
"title" => "User",
|
44
|
+
"description" => "A user of the system",
|
45
|
+
"properties" => {
|
46
|
+
"name" => {
|
47
|
+
"type" => "string", "title" => "Full Name", "description" => "The user's name"
|
48
|
+
},
|
49
|
+
"email" => {
|
50
|
+
"type" => "object",
|
51
|
+
"properties" => {
|
52
|
+
"address" => {
|
53
|
+
"type" => "string", "title" => "Email Address", "description" => "The user's email", "format" => "email"
|
79
54
|
},
|
80
|
-
"
|
81
|
-
|
82
|
-
"items": {
|
83
|
-
"type": "string"
|
84
|
-
},
|
85
|
-
"minItems": 1,
|
86
|
-
"uniqueItems": true,
|
87
|
-
"description": "The user's tags"
|
55
|
+
"verified" => {
|
56
|
+
"type" => "boolean", "description" => "Whether the email is verified"
|
88
57
|
}
|
58
|
+
},
|
59
|
+
"required" => ["address", "verified"]
|
60
|
+
},
|
61
|
+
"group" => {
|
62
|
+
"type" => "integer", "description" => "The user's group", "enum" => [1, 2, 3], "default" => 1
|
63
|
+
},
|
64
|
+
"age" => {
|
65
|
+
"type" => "integer", "description" => "The user's age", "minimum" => 18, "maximum" => 100
|
89
66
|
},
|
90
|
-
"
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
67
|
+
"tags" => {
|
68
|
+
"type" => "array",
|
69
|
+
"items" => { "type" => "string" },
|
70
|
+
"description" => "The user's tags",
|
71
|
+
"minItems" => 1,
|
72
|
+
"uniqueItems" => true
|
73
|
+
}
|
74
|
+
},
|
75
|
+
"required" => ["name", "email", "group", "age", "tags"]
|
97
76
|
}
|
98
77
|
```
|
99
78
|
|
79
|
+
Instantiate a User object and validate it with ActiveModel validations:
|
80
|
+
|
81
|
+
```ruby
|
82
|
+
user = User.new(name: "John Doe", email: { address: "john@test.com", verified: true }, group: 1, age: 25, tags: ["tag1", "tag2"])
|
83
|
+
user.valid? # => true
|
84
|
+
|
85
|
+
user.name = nil
|
86
|
+
user.valid? # => false
|
87
|
+
|
88
|
+
user.errors.full_messages # => ["Name can't be blank"]
|
89
|
+
user.errors["name"] # => ["can't be blank"]
|
90
|
+
```
|
91
|
+
|
100
92
|
## Installation
|
101
93
|
|
102
94
|
install the gem by running the following command in your terminal:
|
@@ -105,12 +97,20 @@ Calling `User.json_schema` will return the JSON Schema for the User class:
|
|
105
97
|
|
106
98
|
## Usage
|
107
99
|
|
108
|
-
Simply include the `EasyTalk::Model` module in your Ruby class, define the schema using the `define_schema` block and call the `json_schema` class method to generate the JSON Schema document.
|
100
|
+
Simply include the `EasyTalk::Model` module in your Ruby class, define the schema using the `define_schema` block, and call the `json_schema` class method to generate the JSON Schema document.
|
109
101
|
|
110
102
|
|
111
103
|
## Schema Definition
|
112
104
|
|
113
|
-
In the example above, the
|
105
|
+
In the example above, the define_schema method adds a title and description to the schema. The property method defines properties of the schema document. property accepts:
|
106
|
+
|
107
|
+
* A name (symbol)
|
108
|
+
* A type (generic Ruby type like String/Integer, a Sorbet type like T::Boolean, or one of the custom types like T::AnyOf[...])
|
109
|
+
* A hash of constraints (e.g., minimum: 18, enum: [1, 2, 3], etc.)
|
110
|
+
|
111
|
+
## Why Sortbet-style types?
|
112
|
+
|
113
|
+
Ruby doesn’t natively allow complex types like Array[String] or Array[Integer]. Sorbet-style types let you define these compound types clearly. EasyTalk uses this style to handle property types such as T::Array[String] or T::AnyOf[ClassA, ClassB].
|
114
114
|
|
115
115
|
## Property Constraints
|
116
116
|
|
@@ -119,88 +119,89 @@ Property constraints are type-dependent. Refer to the [CONSTRAINTS.md](CONSTRAIN
|
|
119
119
|
|
120
120
|
## Schema Composition
|
121
121
|
|
122
|
-
EasyTalk supports schema composition. You can define a schema for a nested object by defining a new class
|
122
|
+
EasyTalk supports schema composition. You can define a schema for a nested object by defining a new class that includes `EasyTalk::Model`. You can then reference the nested schema in the parent using special types:
|
123
|
+
|
124
|
+
T::OneOf[Model1, Model2, ...] — The property must match at least one of the specified schemas
|
125
|
+
T::AnyOf[Model1, Model2, ...] — The property can match any of the specified schemas
|
126
|
+
T::AllOf[Model1, Model2, ...] — The property must match all of the specified schemas
|
123
127
|
|
124
|
-
|
125
|
-
- T::AnyOf[Model1, Model2, ...] - The property can match any of the specified schemas.
|
126
|
-
- T::AllOf[Model1, Model2, ...] - The property must match all of the specified schemas.
|
128
|
+
Example: A Payment object that can be a credit card, PayPal, or bank transfer:
|
127
129
|
|
128
|
-
Here is an example where we define a schema for a payment object that can be a credit card, a PayPal account, or a bank transfer. The first three classes represent the schemas for the different payment methods. The `Payment` class represents the schema for the payment object where the `Details` property can be any of the payment method schemas.
|
129
130
|
|
130
131
|
```ruby
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
end
|
132
|
+
class CreditCard
|
133
|
+
include EasyTalk::Model
|
134
|
+
|
135
|
+
define_schema do
|
136
|
+
property :CardNumber, String
|
137
|
+
property :CardType, String, enum: %w[Visa MasterCard AmericanExpress]
|
138
|
+
property :CardExpMonth, Integer, minimum: 1, maximum: 12
|
139
|
+
property :CardExpYear, Integer, minimum: Date.today.year, maximum: Date.today.year + 10
|
140
|
+
property :CardCVV, String, pattern: '^[0-9]{3,4}$'
|
141
|
+
additional_properties false
|
142
142
|
end
|
143
|
+
end
|
143
144
|
|
144
|
-
|
145
|
-
|
145
|
+
class Paypal
|
146
|
+
include EasyTalk::Model
|
146
147
|
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
end
|
148
|
+
define_schema do
|
149
|
+
property :PaypalEmail, String, format: 'email'
|
150
|
+
property :PaypalPasswordEncrypted, String
|
151
|
+
additional_properties false
|
152
152
|
end
|
153
|
+
end
|
153
154
|
|
154
|
-
|
155
|
-
|
155
|
+
class BankTransfer
|
156
|
+
include EasyTalk::Model
|
156
157
|
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
|
163
|
-
end
|
158
|
+
define_schema do
|
159
|
+
property :BankName, String
|
160
|
+
property :AccountNumber, String
|
161
|
+
property :RoutingNumber, String
|
162
|
+
property :AccountType, String, enum: %w[Checking Savings]
|
163
|
+
additional_properties false
|
164
164
|
end
|
165
|
+
end
|
165
166
|
|
166
|
-
|
167
|
-
|
167
|
+
class Payment
|
168
|
+
include EasyTalk::Model
|
168
169
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
end
|
170
|
+
define_schema do
|
171
|
+
title 'Payment'
|
172
|
+
description 'Payment info'
|
173
|
+
property :PaymentMethod, String, enum: %w[CreditCard Paypal BankTransfer]
|
174
|
+
property :Details, T::AnyOf[CreditCard, Paypal, BankTransfer]
|
175
175
|
end
|
176
|
-
|
176
|
+
end
|
177
177
|
```
|
178
178
|
|
179
179
|
## Type Checking and Schema Constraints
|
180
180
|
|
181
|
-
EasyTalk uses
|
182
|
-
|
183
|
-
EasyTalk raises an error if the constraint values do not match the property type. For example, if you specify the `enum` constraint with the values [1,2,3], but the property type is `String`, EasyTalk will raise a type error.
|
181
|
+
EasyTalk uses a combination of standard Ruby types (`String`, `Integer`), Sorbet types (`T::Boolean`, `T::Array[String]`, etc.), and custom Sorbet-style types (`T::AnyOf[]`, `T::OneOf[]`) to perform basic type checking. For example:
|
184
182
|
|
185
|
-
|
183
|
+
If you specify `enum: [1,2,3]` but the property type is `String`, EasyTalk raises a type error.
|
184
|
+
If you define `minimum: 1` on a `String` property, it raises an error because minimum applies only to numeric types.
|
186
185
|
|
187
186
|
## Schema Validation
|
188
187
|
|
189
|
-
|
190
|
-
|
191
|
-
The goal is to introduce JSON validation in the near future.
|
188
|
+
You can instantiate an EasyTalk model with a hash of attributes and validate it using standard ActiveModel validations. EasyTalk does not automatically validate instances; you must explicitly define ActiveModel validations in your EasyTalk model. See [spec/easy_talk/activemodel_integration_spec.rb](ActiveModel Integration Spec) for examples.
|
192
189
|
|
193
190
|
## JSON Schema Specifications
|
194
191
|
|
195
|
-
EasyTalk is currently
|
192
|
+
EasyTalk is currently loose about JSON Schema versions. It doesn’t strictly enforce or adhere to any particular version of the specification. The goal is to add more robust support for the latest JSON Schema specs in the future.
|
196
193
|
|
197
|
-
To learn about
|
194
|
+
To learn about current capabilities, see the [spec/easy_talk/examples](https://github.com/sergiobayona/easy_talk/tree/main/spec/easy_talk/examples) folder. The examples illustrate how EasyTalk generates JSON Schema in different scenarios.
|
198
195
|
|
199
196
|
## Development
|
200
197
|
|
201
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that
|
198
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that lets you experiment.
|
202
199
|
|
203
|
-
To install this gem onto your local machine, run
|
200
|
+
To install this gem onto your local machine, run:
|
201
|
+
|
202
|
+
```bash
|
203
|
+
bundle exec rake install
|
204
|
+
```
|
204
205
|
|
205
206
|
## Contributing
|
206
207
|
|
@@ -1,15 +1,21 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
1
|
require_relative 'base_builder'
|
2
|
+
require 'set'
|
4
3
|
|
5
4
|
module EasyTalk
|
6
5
|
module Builders
|
7
|
-
#
|
6
|
+
#
|
7
|
+
# ObjectBuilder is responsible for turning a SchemaDefinition of an "object" type
|
8
|
+
# into a validated JSON Schema hash. It:
|
9
|
+
#
|
10
|
+
# 1) Recursively processes the schema’s :properties,
|
11
|
+
# 2) Determines which properties are required (unless nilable or optional),
|
12
|
+
# 3) Handles sub-schema composition (allOf, anyOf, oneOf, not),
|
13
|
+
# 4) Produces the final object-level schema hash.
|
14
|
+
#
|
8
15
|
class ObjectBuilder < BaseBuilder
|
9
16
|
extend T::Sig
|
10
17
|
|
11
|
-
|
12
|
-
|
18
|
+
# Required by BaseBuilder: recognized schema options for "object" types
|
13
19
|
VALID_OPTIONS = {
|
14
20
|
properties: { type: T::Hash[T.any(Symbol, String), T.untyped], key: :properties },
|
15
21
|
additional_properties: { type: T::Boolean, key: :additionalProperties },
|
@@ -24,83 +30,173 @@ module EasyTalk
|
|
24
30
|
|
25
31
|
sig { params(schema_definition: EasyTalk::SchemaDefinition).void }
|
26
32
|
def initialize(schema_definition)
|
33
|
+
# Keep a reference to the original schema definition
|
27
34
|
@schema_definition = schema_definition
|
28
|
-
|
29
|
-
@
|
30
|
-
|
31
|
-
|
35
|
+
# Duplicate the raw schema hash so we can mutate it safely
|
36
|
+
@original_schema = schema_definition.schema.dup
|
37
|
+
|
38
|
+
# We'll collect required property names in this Set
|
39
|
+
@required_properties = Set.new
|
40
|
+
|
41
|
+
# Usually the name is a string (class name). Fallback to :klass if nil.
|
42
|
+
name_for_builder = schema_definition.name ? schema_definition.name.to_sym : :klass
|
43
|
+
|
44
|
+
# Build the base structure: { type: 'object' } plus any top-level options
|
45
|
+
super(
|
46
|
+
name_for_builder,
|
47
|
+
{ type: 'object' }, # minimal "object" structure
|
48
|
+
build_options_hash, # method below merges & cleans final top-level keys
|
49
|
+
VALID_OPTIONS
|
50
|
+
)
|
32
51
|
end
|
33
52
|
|
34
53
|
private
|
35
54
|
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
55
|
+
##
|
56
|
+
# Main aggregator: merges the top-level schema keys (like :properties, :subschemas)
|
57
|
+
# into a single hash that we’ll feed to BaseBuilder.
|
58
|
+
def build_options_hash
|
59
|
+
# Start with a copy of the raw schema
|
60
|
+
merged = @original_schema.dup
|
61
|
+
|
62
|
+
# Extract and build sub-schemas first (handles allOf/anyOf/oneOf references, etc.)
|
63
|
+
process_subschemas(merged)
|
64
|
+
|
65
|
+
# Build :properties into a final form (and find "required" props)
|
66
|
+
merged[:properties] = build_properties(merged.delete(:properties))
|
67
|
+
|
68
|
+
# Populate the final "required" array from @required_properties
|
69
|
+
merged[:required] = @required_properties.to_a if @required_properties.any?
|
70
|
+
|
71
|
+
# Prune empty or nil values so we don't produce stuff like "properties": {} unnecessarily
|
72
|
+
merged.reject! { |_k, v| v.nil? || v == {} || v == [] }
|
73
|
+
|
74
|
+
merged
|
75
|
+
end
|
76
|
+
|
77
|
+
##
|
78
|
+
# Given the property definitions hash, produce a new hash of
|
79
|
+
# { property_name => [Property or nested schema builder result] }.
|
80
|
+
#
|
81
|
+
def build_properties(properties_hash)
|
82
|
+
return {} unless properties_hash.is_a?(Hash)
|
83
|
+
|
84
|
+
# Cache with a key based on property name and its full configuration
|
85
|
+
@properties_cache ||= {}
|
86
|
+
|
87
|
+
properties_hash.each_with_object({}) do |(prop_name, prop_options), result|
|
88
|
+
cache_key = [prop_name, prop_options].hash
|
89
|
+
|
90
|
+
# Use cache if the exact property and configuration have been processed before
|
91
|
+
@properties_cache[cache_key] ||= begin
|
92
|
+
mark_required_unless_optional(prop_name, prop_options)
|
93
|
+
build_property(prop_name, prop_options)
|
42
94
|
end
|
95
|
+
|
96
|
+
result[prop_name] = @properties_cache[cache_key]
|
43
97
|
end
|
44
98
|
end
|
45
99
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
100
|
+
##
|
101
|
+
# Decide if a property should be required. If it's optional or nilable,
|
102
|
+
# we won't include it in the "required" array.
|
103
|
+
#
|
104
|
+
def mark_required_unless_optional(prop_name, prop_options)
|
105
|
+
return if property_optional?(prop_options)
|
106
|
+
|
107
|
+
@required_properties.add(prop_name)
|
108
|
+
end
|
109
|
+
|
110
|
+
##
|
111
|
+
# Returns true if the property is declared optional or is T.nilable(...).
|
112
|
+
#
|
113
|
+
def property_optional?(prop_options)
|
114
|
+
# For convenience, treat :type as an object
|
115
|
+
type_obj = prop_options[:type]
|
116
|
+
|
117
|
+
# Check Sorbet's nilable (like T.nilable(String))
|
118
|
+
return true if type_obj.respond_to?(:nilable?) && type_obj.nilable?
|
119
|
+
|
120
|
+
# Check constraints[:optional]
|
121
|
+
return true if prop_options.dig(:constraints, :optional)
|
51
122
|
|
52
|
-
|
123
|
+
false
|
53
124
|
end
|
54
|
-
# rubocop:enable Style/DoubleNegation
|
55
125
|
|
56
|
-
|
126
|
+
##
|
127
|
+
# Builds a single property. Could be a nested schema if it has sub-properties,
|
128
|
+
# or a standard scalar property (String, Integer, etc.).
|
129
|
+
#
|
130
|
+
def build_property(prop_name, prop_options)
|
57
131
|
@property_cache ||= {}
|
58
132
|
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
133
|
+
# Memoize so we only build each property once
|
134
|
+
@property_cache[prop_name] ||= if prop_options[:properties]
|
135
|
+
# This indicates block-style definition => nested schema
|
136
|
+
nested_schema_builder(prop_options)
|
137
|
+
else
|
138
|
+
# Normal property: e.g. { type: String, constraints: {...} }
|
139
|
+
handle_nilable_type(prop_options)
|
140
|
+
Property.new(prop_name, prop_options[:type], prop_options[:constraints])
|
141
|
+
end
|
65
142
|
end
|
66
143
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
144
|
+
##
|
145
|
+
# Build a child schema by calling another ObjectBuilder on the nested SchemaDefinition.
|
146
|
+
#
|
147
|
+
def nested_schema_builder(prop_options)
|
148
|
+
child_schema_def = prop_options[:properties]
|
149
|
+
# If user used T.nilable(...) with a block, unwrap the nilable
|
150
|
+
handle_nilable_type(prop_options)
|
151
|
+
ObjectBuilder.new(child_schema_def).build
|
71
152
|
end
|
72
153
|
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
154
|
+
##
|
155
|
+
# If the type is T.nilable(SomeType), unwrap it so we produce the correct schema.
|
156
|
+
# This logic is borrowed from the old #handle_option_type method.
|
157
|
+
#
|
158
|
+
def handle_nilable_type(prop_options)
|
159
|
+
type_obj = prop_options[:type]
|
160
|
+
return unless type_obj.respond_to?(:nilable?) && type_obj.nilable?
|
161
|
+
|
162
|
+
# If the underlying raw_type isn't T::Types::TypedArray, then we unwrap it
|
163
|
+
return unless type_obj.unwrap_nilable.class != T::Types::TypedArray
|
164
|
+
|
165
|
+
prop_options[:type] = type_obj.unwrap_nilable.raw_type
|
81
166
|
end
|
82
167
|
|
83
|
-
|
84
|
-
|
85
|
-
|
168
|
+
##
|
169
|
+
# Process top-level composition keywords (e.g. allOf, anyOf, oneOf),
|
170
|
+
# converting them to definitions + references if appropriate.
|
171
|
+
#
|
172
|
+
def process_subschemas(schema_hash)
|
173
|
+
subschemas = schema_hash.delete(:subschemas) || []
|
174
|
+
subschemas.each do |subschema|
|
175
|
+
add_defs_from_subschema(schema_hash, subschema)
|
176
|
+
add_refs_from_subschema(schema_hash, subschema)
|
86
177
|
end
|
87
|
-
schema[:defs] = definitions
|
88
178
|
end
|
89
179
|
|
90
|
-
|
91
|
-
|
92
|
-
|
180
|
+
##
|
181
|
+
# For each item in the composer, add it to :defs so that we can reference it later.
|
182
|
+
#
|
183
|
+
def add_defs_from_subschema(schema_hash, subschema)
|
184
|
+
# Build up a hash of class_name => schema for each sub-item
|
185
|
+
definitions = subschema.items.each_with_object({}) do |item, acc|
|
186
|
+
acc[item.name] = item.schema
|
93
187
|
end
|
94
|
-
|
188
|
+
# Merge or create :defs
|
189
|
+
existing_defs = schema_hash[:defs] || {}
|
190
|
+
schema_hash[:defs] = existing_defs.merge(definitions)
|
95
191
|
end
|
96
192
|
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
193
|
+
##
|
194
|
+
# Add references to the schema for each sub-item in the composer
|
195
|
+
# e.g. { "$ref": "#/$defs/SomeClass" }
|
196
|
+
#
|
197
|
+
def add_refs_from_subschema(schema_hash, subschema)
|
198
|
+
references = subschema.items.map { |item| { '$ref': item.ref_template } }
|
199
|
+
schema_hash[subschema.name] = references
|
104
200
|
end
|
105
201
|
end
|
106
202
|
end
|
data/lib/easy_talk/model.rb
CHANGED
@@ -7,8 +7,6 @@ require 'active_support/time'
|
|
7
7
|
require 'active_support/concern'
|
8
8
|
require 'active_support/json'
|
9
9
|
require 'active_model'
|
10
|
-
require 'json_schemer'
|
11
|
-
require_relative 'schema_errors_mapper'
|
12
10
|
require_relative 'builders/object_builder'
|
13
11
|
require_relative 'schema_definition'
|
14
12
|
|
@@ -39,30 +37,9 @@ module EasyTalk
|
|
39
37
|
base.include ActiveModel::API # Include ActiveModel::API in the class including EasyTalk::Model
|
40
38
|
base.include ActiveModel::Validations
|
41
39
|
base.extend ActiveModel::Callbacks
|
42
|
-
base.validates_with SchemaValidator
|
43
40
|
base.extend(ClassMethods)
|
44
41
|
end
|
45
42
|
|
46
|
-
class SchemaValidator < ActiveModel::Validator
|
47
|
-
def validate(record)
|
48
|
-
result = schema_validation(record)
|
49
|
-
result.errors.each do |key, error_msg|
|
50
|
-
record.errors.add key.to_sym, error_msg
|
51
|
-
end
|
52
|
-
end
|
53
|
-
|
54
|
-
def schema_validation(record)
|
55
|
-
schema = JSONSchemer.schema(record.class.json_schema)
|
56
|
-
errors = schema.validate(record.properties)
|
57
|
-
SchemaErrorsMapper.new(errors)
|
58
|
-
end
|
59
|
-
end
|
60
|
-
|
61
|
-
# Returns the properties of the model as a hash with symbolized keys.
|
62
|
-
def properties
|
63
|
-
as_json.symbolize_keys!
|
64
|
-
end
|
65
|
-
|
66
43
|
# Module containing class-level methods for defining and accessing the schema of a model.
|
67
44
|
module ClassMethods
|
68
45
|
# Returns the schema for the model.
|
@@ -72,13 +49,6 @@ module EasyTalk
|
|
72
49
|
@schema ||= build_schema(schema_definition)
|
73
50
|
end
|
74
51
|
|
75
|
-
# Returns true if the class inherits a schema.
|
76
|
-
#
|
77
|
-
# @return [Boolean] `true` if the class inherits a schema, `false` otherwise.
|
78
|
-
def inherits_schema?
|
79
|
-
false
|
80
|
-
end
|
81
|
-
|
82
52
|
# Returns the reference template for the model.
|
83
53
|
#
|
84
54
|
# @return [String] The reference template for the model.
|
@@ -86,13 +56,6 @@ module EasyTalk
|
|
86
56
|
"#/$defs/#{name}"
|
87
57
|
end
|
88
58
|
|
89
|
-
# Returns the name of the model as a human-readable function name.
|
90
|
-
#
|
91
|
-
# @return [String] The human-readable function name of the model.
|
92
|
-
def function_name
|
93
|
-
name.humanize.titleize
|
94
|
-
end
|
95
|
-
|
96
59
|
def properties
|
97
60
|
@properties ||= begin
|
98
61
|
return unless schema[:properties].present?
|
@@ -119,7 +82,7 @@ module EasyTalk
|
|
119
82
|
@schema_definition.instance_eval(&block)
|
120
83
|
attr_accessor(*properties)
|
121
84
|
|
122
|
-
@
|
85
|
+
@schema_definition
|
123
86
|
end
|
124
87
|
|
125
88
|
# Returns the unvalidated schema definition for the model.
|
@@ -3,12 +3,14 @@
|
|
3
3
|
require_relative 'keywords'
|
4
4
|
|
5
5
|
module EasyTalk
|
6
|
+
class InvalidPropertyNameError < StandardError; end
|
6
7
|
#
|
7
8
|
#= EasyTalk \SchemaDefinition
|
8
9
|
# SchemaDefinition provides the methods for defining a schema within the define_schema block.
|
9
10
|
# The @schema is a hash that contains the unvalidated schema definition for the model.
|
10
11
|
# A SchemaDefinition instanace is the passed to the Builder.build_schema method to validate and compile the schema.
|
11
12
|
class SchemaDefinition
|
13
|
+
|
12
14
|
extend T::Sig
|
13
15
|
extend T::AnyOf
|
14
16
|
extend T::OneOf
|
@@ -35,18 +37,30 @@ module EasyTalk
|
|
35
37
|
sig do
|
36
38
|
params(name: T.any(Symbol, String), type: T.untyped, constraints: T.untyped, blk: T.nilable(T.proc.void)).void
|
37
39
|
end
|
38
|
-
def property(name, type,
|
40
|
+
def property(name, type, constraints = {}, &blk)
|
41
|
+
validate_property_name(name)
|
39
42
|
@schema[:properties] ||= {}
|
40
43
|
|
41
44
|
if block_given?
|
42
|
-
property_schema = SchemaDefinition.new(name
|
45
|
+
property_schema = SchemaDefinition.new(name)
|
43
46
|
property_schema.instance_eval(&blk)
|
44
|
-
|
47
|
+
|
48
|
+
@schema[:properties][name] = {
|
49
|
+
type:,
|
50
|
+
constraints:,
|
51
|
+
properties: property_schema
|
52
|
+
}
|
45
53
|
else
|
46
54
|
@schema[:properties][name] = { type:, constraints: }
|
47
55
|
end
|
48
56
|
end
|
49
57
|
|
58
|
+
def validate_property_name(name)
|
59
|
+
unless name.to_s.match?(/^[A-Za-z_][A-Za-z0-9_]*$/)
|
60
|
+
raise InvalidPropertyNameError, "Invalid property name '#{name}'. Must start with letter/underscore and contain only letters, numbers, underscores"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
50
64
|
def optional?
|
51
65
|
@schema[:optional]
|
52
66
|
end
|
@@ -5,23 +5,35 @@ module EasyTalk
|
|
5
5
|
# FunctionBuilder is a module that builds a hash with the function type and function details.
|
6
6
|
# The return value is typically passed as argument to LLM function calling APIs.
|
7
7
|
module FunctionBuilder
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
8
|
+
class << self
|
9
|
+
# Creates a new function object based on the given model.
|
10
|
+
#
|
11
|
+
# @param [Model] model The EasyTalk model containing the function details.
|
12
|
+
# @return [Hash] The function object.
|
13
|
+
def new(model)
|
14
|
+
{
|
15
|
+
type: 'function',
|
16
|
+
function: {
|
17
|
+
name: generate_function_name(model),
|
18
|
+
description: generate_function_description(model),
|
19
|
+
parameters: model.json_schema
|
20
|
+
}
|
19
21
|
}
|
20
|
-
|
21
|
-
|
22
|
+
end
|
23
|
+
|
24
|
+
def generate_function_name(model)
|
25
|
+
model.schema.fetch(:title, model.name)
|
26
|
+
end
|
27
|
+
|
28
|
+
def generate_function_description(model)
|
29
|
+
if model.respond_to?(:instructions)
|
30
|
+
raise Instructor::Error, 'The instructions must be a string' unless model.instructions.is_a?(String)
|
22
31
|
|
23
|
-
|
24
|
-
|
32
|
+
model.instructions
|
33
|
+
else
|
34
|
+
"Correctly extracted `#{model.name}` with all the required parameters and correct types."
|
35
|
+
end
|
36
|
+
end
|
25
37
|
end
|
26
38
|
end
|
27
39
|
end
|
data/lib/easy_talk/version.rb
CHANGED
metadata
CHANGED
@@ -1,69 +1,54 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: easy_talk
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sergio Bayona
|
8
|
-
autorequire:
|
9
8
|
bindir: bin
|
10
9
|
cert_chain: []
|
11
|
-
date:
|
10
|
+
date: 2025-01-09 00:00:00.000000000 Z
|
12
11
|
dependencies:
|
13
12
|
- !ruby/object:Gem::Dependency
|
14
13
|
name: activemodel
|
15
14
|
requirement: !ruby/object:Gem::Requirement
|
16
15
|
requirements:
|
17
|
-
- - "
|
16
|
+
- - ">="
|
18
17
|
- !ruby/object:Gem::Version
|
19
18
|
version: '7.0'
|
20
19
|
type: :runtime
|
21
20
|
prerelease: false
|
22
21
|
version_requirements: !ruby/object:Gem::Requirement
|
23
22
|
requirements:
|
24
|
-
- - "
|
23
|
+
- - ">="
|
25
24
|
- !ruby/object:Gem::Version
|
26
25
|
version: '7.0'
|
27
26
|
- !ruby/object:Gem::Dependency
|
28
27
|
name: activesupport
|
29
|
-
requirement: !ruby/object:Gem::Requirement
|
30
|
-
requirements:
|
31
|
-
- - "~>"
|
32
|
-
- !ruby/object:Gem::Version
|
33
|
-
version: '7.0'
|
34
|
-
type: :runtime
|
35
|
-
prerelease: false
|
36
|
-
version_requirements: !ruby/object:Gem::Requirement
|
37
|
-
requirements:
|
38
|
-
- - "~>"
|
39
|
-
- !ruby/object:Gem::Version
|
40
|
-
version: '7.0'
|
41
|
-
- !ruby/object:Gem::Dependency
|
42
|
-
name: json_schemer
|
43
28
|
requirement: !ruby/object:Gem::Requirement
|
44
29
|
requirements:
|
45
30
|
- - ">="
|
46
31
|
- !ruby/object:Gem::Version
|
47
|
-
version: '0'
|
32
|
+
version: '7.0'
|
48
33
|
type: :runtime
|
49
34
|
prerelease: false
|
50
35
|
version_requirements: !ruby/object:Gem::Requirement
|
51
36
|
requirements:
|
52
37
|
- - ">="
|
53
38
|
- !ruby/object:Gem::Version
|
54
|
-
version: '0'
|
39
|
+
version: '7.0'
|
55
40
|
- !ruby/object:Gem::Dependency
|
56
41
|
name: sorbet-runtime
|
57
42
|
requirement: !ruby/object:Gem::Requirement
|
58
43
|
requirements:
|
59
|
-
- - "
|
44
|
+
- - ">="
|
60
45
|
- !ruby/object:Gem::Version
|
61
46
|
version: '0.5'
|
62
47
|
type: :runtime
|
63
48
|
prerelease: false
|
64
49
|
version_requirements: !ruby/object:Gem::Requirement
|
65
50
|
requirements:
|
66
|
-
- - "
|
51
|
+
- - ">="
|
67
52
|
- !ruby/object:Gem::Version
|
68
53
|
version: '0.5'
|
69
54
|
- !ruby/object:Gem::Dependency
|
@@ -84,98 +69,98 @@ dependencies:
|
|
84
69
|
name: rake
|
85
70
|
requirement: !ruby/object:Gem::Requirement
|
86
71
|
requirements:
|
87
|
-
- - "
|
72
|
+
- - ">="
|
88
73
|
- !ruby/object:Gem::Version
|
89
74
|
version: '13.1'
|
90
75
|
type: :development
|
91
76
|
prerelease: false
|
92
77
|
version_requirements: !ruby/object:Gem::Requirement
|
93
78
|
requirements:
|
94
|
-
- - "
|
79
|
+
- - ">="
|
95
80
|
- !ruby/object:Gem::Version
|
96
81
|
version: '13.1'
|
97
82
|
- !ruby/object:Gem::Dependency
|
98
83
|
name: rspec
|
99
84
|
requirement: !ruby/object:Gem::Requirement
|
100
85
|
requirements:
|
101
|
-
- - "
|
86
|
+
- - ">="
|
102
87
|
- !ruby/object:Gem::Version
|
103
88
|
version: '3.0'
|
104
89
|
type: :development
|
105
90
|
prerelease: false
|
106
91
|
version_requirements: !ruby/object:Gem::Requirement
|
107
92
|
requirements:
|
108
|
-
- - "
|
93
|
+
- - ">="
|
109
94
|
- !ruby/object:Gem::Version
|
110
95
|
version: '3.0'
|
111
96
|
- !ruby/object:Gem::Dependency
|
112
97
|
name: rspec-json_expectations
|
113
98
|
requirement: !ruby/object:Gem::Requirement
|
114
99
|
requirements:
|
115
|
-
- - "
|
100
|
+
- - ">="
|
116
101
|
- !ruby/object:Gem::Version
|
117
102
|
version: '2.0'
|
118
103
|
type: :development
|
119
104
|
prerelease: false
|
120
105
|
version_requirements: !ruby/object:Gem::Requirement
|
121
106
|
requirements:
|
122
|
-
- - "
|
107
|
+
- - ">="
|
123
108
|
- !ruby/object:Gem::Version
|
124
109
|
version: '2.0'
|
125
110
|
- !ruby/object:Gem::Dependency
|
126
111
|
name: rspec-mocks
|
127
112
|
requirement: !ruby/object:Gem::Requirement
|
128
113
|
requirements:
|
129
|
-
- - "
|
114
|
+
- - ">="
|
130
115
|
- !ruby/object:Gem::Version
|
131
116
|
version: '3.13'
|
132
117
|
type: :development
|
133
118
|
prerelease: false
|
134
119
|
version_requirements: !ruby/object:Gem::Requirement
|
135
120
|
requirements:
|
136
|
-
- - "
|
121
|
+
- - ">="
|
137
122
|
- !ruby/object:Gem::Version
|
138
123
|
version: '3.13'
|
139
124
|
- !ruby/object:Gem::Dependency
|
140
125
|
name: rubocop
|
141
126
|
requirement: !ruby/object:Gem::Requirement
|
142
127
|
requirements:
|
143
|
-
- - "
|
128
|
+
- - ">="
|
144
129
|
- !ruby/object:Gem::Version
|
145
130
|
version: '1.21'
|
146
131
|
type: :development
|
147
132
|
prerelease: false
|
148
133
|
version_requirements: !ruby/object:Gem::Requirement
|
149
134
|
requirements:
|
150
|
-
- - "
|
135
|
+
- - ">="
|
151
136
|
- !ruby/object:Gem::Version
|
152
137
|
version: '1.21'
|
153
138
|
- !ruby/object:Gem::Dependency
|
154
139
|
name: rubocop-rake
|
155
140
|
requirement: !ruby/object:Gem::Requirement
|
156
141
|
requirements:
|
157
|
-
- - "
|
142
|
+
- - ">="
|
158
143
|
- !ruby/object:Gem::Version
|
159
144
|
version: '0.6'
|
160
145
|
type: :development
|
161
146
|
prerelease: false
|
162
147
|
version_requirements: !ruby/object:Gem::Requirement
|
163
148
|
requirements:
|
164
|
-
- - "
|
149
|
+
- - ">="
|
165
150
|
- !ruby/object:Gem::Version
|
166
151
|
version: '0.6'
|
167
152
|
- !ruby/object:Gem::Dependency
|
168
153
|
name: rubocop-rspec
|
169
154
|
requirement: !ruby/object:Gem::Requirement
|
170
155
|
requirements:
|
171
|
-
- - "
|
156
|
+
- - ">="
|
172
157
|
- !ruby/object:Gem::Version
|
173
158
|
version: '2.29'
|
174
159
|
type: :development
|
175
160
|
prerelease: false
|
176
161
|
version_requirements: !ruby/object:Gem::Requirement
|
177
162
|
requirements:
|
178
|
-
- - "
|
163
|
+
- - ">="
|
179
164
|
- !ruby/object:Gem::Version
|
180
165
|
version: '2.29'
|
181
166
|
description: Generate json-schema from plain Ruby classes.
|
@@ -220,7 +205,6 @@ files:
|
|
220
205
|
- lib/easy_talk/model.rb
|
221
206
|
- lib/easy_talk/property.rb
|
222
207
|
- lib/easy_talk/schema_definition.rb
|
223
|
-
- lib/easy_talk/schema_errors_mapper.rb
|
224
208
|
- lib/easy_talk/sorbet_extension.rb
|
225
209
|
- lib/easy_talk/tools/function_builder.rb
|
226
210
|
- lib/easy_talk/types/all_of.rb
|
@@ -236,7 +220,6 @@ metadata:
|
|
236
220
|
homepage_uri: https://github.com/sergiobayona/easy_talk
|
237
221
|
source_code_uri: https://github.com/sergiobayona/easy_talk
|
238
222
|
changelog_uri: https://github.com/sergiobayona/easy_talk/blob/main/CHANGELOG.md
|
239
|
-
post_install_message:
|
240
223
|
rdoc_options: []
|
241
224
|
require_paths:
|
242
225
|
- lib
|
@@ -251,8 +234,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
251
234
|
- !ruby/object:Gem::Version
|
252
235
|
version: '0'
|
253
236
|
requirements: []
|
254
|
-
rubygems_version: 3.
|
255
|
-
signing_key:
|
237
|
+
rubygems_version: 3.6.2
|
256
238
|
specification_version: 4
|
257
239
|
summary: Generate json-schema from Ruby classes.
|
258
240
|
test_files: []
|
@@ -1,23 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module EasyTalk
|
4
|
-
class SchemaErrorsMapper
|
5
|
-
def initialize(errors)
|
6
|
-
@errors = errors.to_a
|
7
|
-
end
|
8
|
-
|
9
|
-
def errors
|
10
|
-
@errors.each_with_object({}) do |error, hash|
|
11
|
-
if error['data_pointer'].present?
|
12
|
-
key = error['data_pointer'].split('/').compact_blank.join('.')
|
13
|
-
hash[key] = error['error']
|
14
|
-
else
|
15
|
-
error['details']['missing_keys'].each do |missing_key|
|
16
|
-
message = "#{error['error'].split(':').first}: #{missing_key}"
|
17
|
-
hash[missing_key] = message
|
18
|
-
end
|
19
|
-
end
|
20
|
-
end
|
21
|
-
end
|
22
|
-
end
|
23
|
-
end
|