ruby_llm-schema 0.1.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 +7 -0
- data/.rspec +3 -0
- data/.rspec_status +55 -0
- data/LICENSE.txt +21 -0
- data/README.md +221 -0
- data/Rakefile +5 -0
- data/lib/ruby_llm/schema/errors.rb +31 -0
- data/lib/ruby_llm/schema/helpers.rb +10 -0
- data/lib/ruby_llm/schema/property_schema_collector.rb +41 -0
- data/lib/ruby_llm/schema/validator.rb +95 -0
- data/lib/ruby_llm/schema/version.rb +7 -0
- data/lib/ruby_llm/schema.rb +218 -0
- data/lib/tasks/release.rake +13 -0
- metadata +83 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: f2af8bd6f95696f6f8ab70e9d83637772bc1cdd3bf6fea283d2d7389a31a156e
|
4
|
+
data.tar.gz: ae39967ea90ec3a189eb0399bcc58da9d3da33d94093d98934bdda71485d70b0
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 5d5903e14886b65431d92dfd511a61db51a824b03da19caf6fe062d1384ae12f4ec1a5004f2b1d2974ed1b32da8b91ca45243c5b299014b4a845828d4722506f
|
7
|
+
data.tar.gz: bb501057362585d42a11abfdc4de38a7743b8944699fcfd24e234655f478d6999c49c3d2659a7538e89d279a56f7dfea51e1577694f1941b8b97fbfa65758a44
|
data/.rspec
ADDED
data/.rspec_status
ADDED
@@ -0,0 +1,55 @@
|
|
1
|
+
example_id | status | run_time |
|
2
|
+
--------------------------------------------------------- | ------ | --------------- |
|
3
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:1:1] | passed | 0.0002 seconds |
|
4
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:1:2] | passed | 0.00004 seconds |
|
5
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:2:1] | passed | 0.00003 seconds |
|
6
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:2:2] | passed | 0.00027 seconds |
|
7
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:3:1] | passed | 0.00004 seconds |
|
8
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:3:2] | passed | 0.00003 seconds |
|
9
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:4:1] | passed | 0.00003 seconds |
|
10
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:1:4:2] | passed | 0.00003 seconds |
|
11
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:2:1] | passed | 0.00044 seconds |
|
12
|
+
./spec/ruby_llm/schema_class_inheritance_spec.rb[1:3:1] | passed | 0.00058 seconds |
|
13
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:1:1] | passed | 0.00004 seconds |
|
14
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:1:2] | passed | 0.00003 seconds |
|
15
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:2:1] | passed | 0.00004 seconds |
|
16
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:2:2] | passed | 0.00003 seconds |
|
17
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:3:1] | passed | 0.00003 seconds |
|
18
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:3:2] | passed | 0.00003 seconds |
|
19
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:4:1] | passed | 0.00003 seconds |
|
20
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:1:4:2] | passed | 0.00003 seconds |
|
21
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:2:1] | passed | 0.00005 seconds |
|
22
|
+
./spec/ruby_llm/schema_factory_spec.rb[1:3:1] | passed | 0.00004 seconds |
|
23
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:1:1] | passed | 0.00004 seconds |
|
24
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:1:2] | passed | 0.00003 seconds |
|
25
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:2:1] | passed | 0.00003 seconds |
|
26
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:2:2] | passed | 0.00003 seconds |
|
27
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:3:1] | passed | 0.00003 seconds |
|
28
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:3:2] | passed | 0.00003 seconds |
|
29
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:4:1] | passed | 0.00003 seconds |
|
30
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:1:4:2] | passed | 0.00003 seconds |
|
31
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:2:1] | passed | 0.00005 seconds |
|
32
|
+
./spec/ruby_llm/schema_helpers_spec.rb[1:3:1] | passed | 0.00003 seconds |
|
33
|
+
./spec/ruby_llm/schema_spec.rb[1:1:1] | passed | 0.00018 seconds |
|
34
|
+
./spec/ruby_llm/schema_spec.rb[1:1:2] | passed | 0.00004 seconds |
|
35
|
+
./spec/ruby_llm/schema_spec.rb[1:1:3] | passed | 0.00003 seconds |
|
36
|
+
./spec/ruby_llm/schema_spec.rb[1:1:4] | passed | 0.00002 seconds |
|
37
|
+
./spec/ruby_llm/schema_spec.rb[1:1:5] | passed | 0.00002 seconds |
|
38
|
+
./spec/ruby_llm/schema_spec.rb[1:1:6] | passed | 0.0006 seconds |
|
39
|
+
./spec/ruby_llm/schema_spec.rb[1:2:1] | passed | 0.00004 seconds |
|
40
|
+
./spec/ruby_llm/schema_spec.rb[1:2:2] | passed | 0.00007 seconds |
|
41
|
+
./spec/ruby_llm/schema_spec.rb[1:2:3] | passed | 0.00004 seconds |
|
42
|
+
./spec/ruby_llm/schema_spec.rb[1:3:1] | passed | 0.00005 seconds |
|
43
|
+
./spec/ruby_llm/schema_spec.rb[1:3:2] | passed | 0.00004 seconds |
|
44
|
+
./spec/ruby_llm/schema_spec.rb[1:3:3] | passed | 0.00004 seconds |
|
45
|
+
./spec/ruby_llm/schema_spec.rb[1:4:1] | passed | 0.00006 seconds |
|
46
|
+
./spec/ruby_llm/schema_spec.rb[1:5:1] | passed | 0.00003 seconds |
|
47
|
+
./spec/ruby_llm/schema_spec.rb[1:5:2] | passed | 0.00057 seconds |
|
48
|
+
./spec/ruby_llm/schema_spec.rb[1:5:3] | passed | 0.00023 seconds |
|
49
|
+
./spec/ruby_llm/schema_spec.rb[1:6:1] | passed | 0.00038 seconds |
|
50
|
+
./spec/ruby_llm/schema_spec.rb[1:6:2] | passed | 0.00003 seconds |
|
51
|
+
./spec/ruby_llm/schema_spec.rb[1:7:1:1] | passed | 0.00021 seconds |
|
52
|
+
./spec/ruby_llm/schema_spec.rb[1:7:1:2] | passed | 0.00006 seconds |
|
53
|
+
./spec/ruby_llm/schema_spec.rb[1:7:2:1] | passed | 0.00005 seconds |
|
54
|
+
./spec/ruby_llm/schema_spec.rb[1:8:1] | passed | 0.00044 seconds |
|
55
|
+
./spec/ruby_llm/schema_spec.rb[1:8:2] | passed | 0.00008 seconds |
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Daniel Friis
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
# RubyLLM::Schema
|
2
|
+
|
3
|
+
A Ruby DSL for creating JSON schemas with a clean, Rails-inspired API. Perfect for defining structured data schemas for LLM function calling or structured outputs.
|
4
|
+
|
5
|
+
### Simple Example
|
6
|
+
|
7
|
+
```ruby
|
8
|
+
class PersonSchema < RubyLLM::Schema
|
9
|
+
string :name, description: "Person's full name"
|
10
|
+
number :age, description: "Age in years"
|
11
|
+
boolean :active, required: false
|
12
|
+
|
13
|
+
object :address do
|
14
|
+
string :street
|
15
|
+
string :city
|
16
|
+
string :country, required: false
|
17
|
+
end
|
18
|
+
|
19
|
+
array :tags, of: :string, description: "User tags"
|
20
|
+
|
21
|
+
array :contacts do
|
22
|
+
object do
|
23
|
+
string :email
|
24
|
+
string :phone, required: false
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
any_of :status do
|
29
|
+
string enum: ["active", "pending", "inactive"]
|
30
|
+
null
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
# Usage
|
35
|
+
schema = PersonSchema.new
|
36
|
+
puts schema.to_json
|
37
|
+
```
|
38
|
+
|
39
|
+
## Installation
|
40
|
+
|
41
|
+
Add this line to your application's Gemfile:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
gem 'ruby_llm-schema'
|
45
|
+
```
|
46
|
+
|
47
|
+
And then execute:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
bundle install
|
51
|
+
```
|
52
|
+
|
53
|
+
Or install it yourself as:
|
54
|
+
|
55
|
+
```bash
|
56
|
+
gem install ruby_llm-schema
|
57
|
+
```
|
58
|
+
|
59
|
+
## Usage
|
60
|
+
|
61
|
+
Three approaches for creating schemas:
|
62
|
+
|
63
|
+
### Class Inheritance
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
class PersonSchema < RubyLLM::Schema
|
67
|
+
string :name, description: "Person's full name"
|
68
|
+
number :age
|
69
|
+
boolean :active, required: false
|
70
|
+
|
71
|
+
object :address do
|
72
|
+
string :street
|
73
|
+
string :city
|
74
|
+
end
|
75
|
+
|
76
|
+
array :tags, of: :string
|
77
|
+
end
|
78
|
+
|
79
|
+
schema = PersonSchema.new
|
80
|
+
puts schema.to_json
|
81
|
+
```
|
82
|
+
|
83
|
+
### Factory Method
|
84
|
+
|
85
|
+
```ruby
|
86
|
+
PersonSchema = RubyLLM::Schema.create do
|
87
|
+
string :name, description: "Person's full name"
|
88
|
+
number :age
|
89
|
+
boolean :active, required: false
|
90
|
+
|
91
|
+
object :address do
|
92
|
+
string :street
|
93
|
+
string :city
|
94
|
+
end
|
95
|
+
|
96
|
+
array :tags, of: :string
|
97
|
+
end
|
98
|
+
|
99
|
+
schema = PersonSchema.new
|
100
|
+
puts schema.to_json
|
101
|
+
```
|
102
|
+
|
103
|
+
### Global Helper
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
require 'ruby_llm/schema'
|
107
|
+
include RubyLLM::Helpers
|
108
|
+
|
109
|
+
person_schema = schema "PersonData", description: "A person object" do
|
110
|
+
string :name, description: "Person's full name"
|
111
|
+
number :age
|
112
|
+
boolean :active, required: false
|
113
|
+
|
114
|
+
object :address do
|
115
|
+
string :street
|
116
|
+
string :city
|
117
|
+
end
|
118
|
+
|
119
|
+
array :tags, of: :string
|
120
|
+
end
|
121
|
+
|
122
|
+
puts person_schema.to_json
|
123
|
+
```
|
124
|
+
|
125
|
+
## Field Types
|
126
|
+
|
127
|
+
### Primitive Types
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
string :name # Required string
|
131
|
+
string :title, required: false # Optional string
|
132
|
+
string :status, enum: ["on", "off"] # String with enum values
|
133
|
+
number :count # Required number
|
134
|
+
integer :id # Required integer
|
135
|
+
boolean :active # Required boolean
|
136
|
+
null :placeholder # Null type
|
137
|
+
```
|
138
|
+
|
139
|
+
### Arrays
|
140
|
+
|
141
|
+
```ruby
|
142
|
+
array :tags, of: :string # Array of strings
|
143
|
+
array :scores, of: :number # Array of numbers
|
144
|
+
|
145
|
+
array :items do # Array of objects
|
146
|
+
object do
|
147
|
+
string :name
|
148
|
+
number :price
|
149
|
+
end
|
150
|
+
end
|
151
|
+
```
|
152
|
+
|
153
|
+
### Objects
|
154
|
+
|
155
|
+
```ruby
|
156
|
+
object :user do
|
157
|
+
string :name
|
158
|
+
number :age
|
159
|
+
end
|
160
|
+
|
161
|
+
object :settings, description: "User preferences" do
|
162
|
+
boolean :notifications
|
163
|
+
string :theme, enum: ["light", "dark"]
|
164
|
+
end
|
165
|
+
```
|
166
|
+
|
167
|
+
### Union Types (anyOf)
|
168
|
+
|
169
|
+
```ruby
|
170
|
+
any_of :value do
|
171
|
+
string
|
172
|
+
number
|
173
|
+
null
|
174
|
+
end
|
175
|
+
|
176
|
+
any_of :identifier do
|
177
|
+
string description: "Username"
|
178
|
+
number description: "User ID"
|
179
|
+
end
|
180
|
+
```
|
181
|
+
|
182
|
+
### Schema Definitions and References
|
183
|
+
|
184
|
+
```ruby
|
185
|
+
class MySchema < RubyLLM::Schema
|
186
|
+
define :location do
|
187
|
+
string :latitude
|
188
|
+
string :longitude
|
189
|
+
end
|
190
|
+
|
191
|
+
array :coordinates, of: :location
|
192
|
+
|
193
|
+
object :home_location do
|
194
|
+
reference :location
|
195
|
+
end
|
196
|
+
end
|
197
|
+
```
|
198
|
+
|
199
|
+
## JSON Output
|
200
|
+
|
201
|
+
```ruby
|
202
|
+
schema = PersonSchema.new
|
203
|
+
schema.to_json_schema
|
204
|
+
# => {
|
205
|
+
# name: "PersonSchema",
|
206
|
+
# description: nil,
|
207
|
+
# schema: {
|
208
|
+
# type: "object",
|
209
|
+
# properties: { ... },
|
210
|
+
# required: [...],
|
211
|
+
# additionalProperties: false,
|
212
|
+
# strict: true
|
213
|
+
# }
|
214
|
+
# }
|
215
|
+
|
216
|
+
puts schema.to_json # Pretty JSON string
|
217
|
+
```
|
218
|
+
|
219
|
+
## License
|
220
|
+
|
221
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
class Schema
|
5
|
+
# Base error class for all schema-related errors
|
6
|
+
class Error < StandardError; end
|
7
|
+
|
8
|
+
# Raised when an invalid schema type is specified
|
9
|
+
class InvalidSchemaTypeError < Error
|
10
|
+
def initialize(type)
|
11
|
+
super("Unknown schema type: #{type}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Raised when an invalid array type is specified
|
16
|
+
class InvalidArrayTypeError < Error
|
17
|
+
def initialize(type)
|
18
|
+
super("Invalid array type: #{type}")
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Raised when schema definition is invalid
|
23
|
+
class InvalidSchemaError < Error; end
|
24
|
+
|
25
|
+
# Raised when schema validation fails
|
26
|
+
class ValidationError < Error; end
|
27
|
+
|
28
|
+
# Raised when maximum limits are exceeded
|
29
|
+
class LimitExceededError < Error; end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
class Schema
|
5
|
+
class PropertySchemaCollector
|
6
|
+
attr_reader :schemas
|
7
|
+
|
8
|
+
def initialize
|
9
|
+
@schemas = []
|
10
|
+
end
|
11
|
+
|
12
|
+
def collect(&)
|
13
|
+
instance_eval(&)
|
14
|
+
end
|
15
|
+
|
16
|
+
def string(**options)
|
17
|
+
@schemas << Schema.build_property_schema(:string, **options)
|
18
|
+
end
|
19
|
+
|
20
|
+
def number(**options)
|
21
|
+
@schemas << Schema.build_property_schema(:number, **options)
|
22
|
+
end
|
23
|
+
|
24
|
+
def integer(**options)
|
25
|
+
@schemas << Schema.build_property_schema(:integer, **options)
|
26
|
+
end
|
27
|
+
|
28
|
+
def boolean(**options)
|
29
|
+
@schemas << Schema.build_property_schema(:boolean, **options)
|
30
|
+
end
|
31
|
+
|
32
|
+
def null(**options)
|
33
|
+
@schemas << Schema.build_property_schema(:null, **options)
|
34
|
+
end
|
35
|
+
|
36
|
+
def object(...)
|
37
|
+
@schemas << Schema.build_property_schema(:object, ...)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyLLM
|
4
|
+
class Schema
|
5
|
+
class Validator
|
6
|
+
# Node states for DFS-based topological sort
|
7
|
+
WHITE = :white # No mark (unvisited)
|
8
|
+
GRAY = :gray # Temporary mark (currently being processed)
|
9
|
+
BLACK = :black # Permanent mark (completely processed)
|
10
|
+
|
11
|
+
def initialize(schema_class)
|
12
|
+
@schema_class = schema_class
|
13
|
+
end
|
14
|
+
|
15
|
+
def validate!
|
16
|
+
validate_circular_references!
|
17
|
+
# Future validations can be added here
|
18
|
+
end
|
19
|
+
|
20
|
+
def valid?
|
21
|
+
validate!
|
22
|
+
true
|
23
|
+
rescue ValidationError
|
24
|
+
false
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
def validate_circular_references!
|
30
|
+
definitions = @schema_class.definitions
|
31
|
+
return if definitions.empty?
|
32
|
+
|
33
|
+
# Initialize all nodes as WHITE (no mark)
|
34
|
+
marks = Hash.new { WHITE }
|
35
|
+
|
36
|
+
# Visit each unmarked node
|
37
|
+
definitions.each_key do |node|
|
38
|
+
visit(node, definitions, marks) if marks[node] == WHITE
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
# DFS visit function
|
43
|
+
def visit(node, definitions, marks)
|
44
|
+
# If node has a permanent mark, return
|
45
|
+
return if marks[node] == BLACK
|
46
|
+
|
47
|
+
# If node has a temporary mark, we found a cycle
|
48
|
+
if marks[node] == GRAY
|
49
|
+
raise ValidationError, "Circular reference detected involving '#{node}'"
|
50
|
+
end
|
51
|
+
|
52
|
+
# Mark node with temporary mark
|
53
|
+
marks[node] = GRAY
|
54
|
+
|
55
|
+
# Visit all adjacent nodes (dependencies)
|
56
|
+
definition = definitions[node]
|
57
|
+
if definition && definition[:properties]
|
58
|
+
definition[:properties].each_value do |property|
|
59
|
+
references = extract_references(property)
|
60
|
+
references.each do |adjacent_node|
|
61
|
+
visit(adjacent_node, definitions, marks)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
# Mark node with permanent mark
|
67
|
+
marks[node] = BLACK
|
68
|
+
end
|
69
|
+
|
70
|
+
def extract_references(property)
|
71
|
+
references = []
|
72
|
+
|
73
|
+
case property
|
74
|
+
when Hash
|
75
|
+
if property["$ref"]
|
76
|
+
# Extract definition name from reference like "#/$defs/user"
|
77
|
+
ref_name = property["$ref"].split("/").last&.to_sym
|
78
|
+
references << ref_name if ref_name
|
79
|
+
else
|
80
|
+
# Recursively check nested properties
|
81
|
+
property.each_value do |value|
|
82
|
+
references.concat(extract_references(value))
|
83
|
+
end
|
84
|
+
end
|
85
|
+
when Array
|
86
|
+
property.each do |item|
|
87
|
+
references.concat(extract_references(item))
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
references
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,218 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "schema/version"
|
4
|
+
require_relative "schema/property_schema_collector"
|
5
|
+
require_relative "schema/errors"
|
6
|
+
require_relative "schema/helpers"
|
7
|
+
require_relative "schema/validator"
|
8
|
+
require "json"
|
9
|
+
require "set"
|
10
|
+
|
11
|
+
module RubyLLM
|
12
|
+
class Schema
|
13
|
+
PRIMITIVE_TYPES = %i[string number integer boolean null].freeze
|
14
|
+
|
15
|
+
class << self
|
16
|
+
def create(&block)
|
17
|
+
schema_class = Class.new(Schema)
|
18
|
+
schema_class.class_eval(&block)
|
19
|
+
schema_class
|
20
|
+
end
|
21
|
+
|
22
|
+
def description(description = nil)
|
23
|
+
@description = description if description
|
24
|
+
@description
|
25
|
+
end
|
26
|
+
|
27
|
+
def additional_properties(value = nil)
|
28
|
+
return @additional_properties ||= false if value.nil?
|
29
|
+
@additional_properties = value
|
30
|
+
end
|
31
|
+
|
32
|
+
def strict(value = nil)
|
33
|
+
return @strict ||= true if value.nil?
|
34
|
+
@strict = value
|
35
|
+
end
|
36
|
+
|
37
|
+
def string(name = nil, enum: nil, description: nil, required: true)
|
38
|
+
add_property(name, build_property_schema(:string, enum: enum, description: description), required: required)
|
39
|
+
end
|
40
|
+
|
41
|
+
def number(name = nil, description: nil, required: true)
|
42
|
+
add_property(name, build_property_schema(:number, description: description), required: required)
|
43
|
+
end
|
44
|
+
|
45
|
+
def integer(name = nil, description: nil, required: true)
|
46
|
+
add_property(name, build_property_schema(:integer, description: description), required: required)
|
47
|
+
end
|
48
|
+
|
49
|
+
def boolean(name = nil, description: nil, required: true)
|
50
|
+
add_property(name, build_property_schema(:boolean, description: description), required: required)
|
51
|
+
end
|
52
|
+
|
53
|
+
def null(name = nil, description: nil, required: true)
|
54
|
+
add_property(name, build_property_schema(:null, description: description), required: required)
|
55
|
+
end
|
56
|
+
|
57
|
+
def object(name = nil, description: nil, required: true, &block)
|
58
|
+
add_property(name, build_property_schema(:object, description: description, &block), required: required)
|
59
|
+
end
|
60
|
+
|
61
|
+
def array(name, of: nil, description: nil, required: true, &block)
|
62
|
+
items = determine_array_items(of, &block)
|
63
|
+
|
64
|
+
add_property(name, {
|
65
|
+
type: "array",
|
66
|
+
description: description,
|
67
|
+
items: items
|
68
|
+
}.compact, required: required)
|
69
|
+
end
|
70
|
+
|
71
|
+
def any_of(name, required: true, description: nil, &block)
|
72
|
+
schemas = collect_property_schemas_from_block(&block)
|
73
|
+
|
74
|
+
add_property(name, {
|
75
|
+
description: description,
|
76
|
+
anyOf: schemas
|
77
|
+
}.compact, required: required)
|
78
|
+
end
|
79
|
+
|
80
|
+
def define(name, &)
|
81
|
+
sub_schema = Class.new(Schema)
|
82
|
+
sub_schema.class_eval(&)
|
83
|
+
|
84
|
+
definitions[name] = {
|
85
|
+
type: "object",
|
86
|
+
properties: sub_schema.properties,
|
87
|
+
required: sub_schema.required_properties
|
88
|
+
}
|
89
|
+
end
|
90
|
+
|
91
|
+
def reference(schema_name)
|
92
|
+
{"$ref" => "#/$defs/#{schema_name}"}
|
93
|
+
end
|
94
|
+
|
95
|
+
def properties
|
96
|
+
@properties ||= {}
|
97
|
+
end
|
98
|
+
|
99
|
+
def required_properties
|
100
|
+
@required_properties ||= []
|
101
|
+
end
|
102
|
+
|
103
|
+
def definitions
|
104
|
+
@definitions ||= {}
|
105
|
+
end
|
106
|
+
|
107
|
+
def build_property_schema(type, **options, &)
|
108
|
+
case type
|
109
|
+
when :string
|
110
|
+
{type: "string", enum: options[:enum], description: options[:description]}.compact
|
111
|
+
when :number
|
112
|
+
{type: "number", description: options[:description]}.compact
|
113
|
+
when :integer
|
114
|
+
{type: "integer", description: options[:description]}.compact
|
115
|
+
when :boolean
|
116
|
+
{type: "boolean", description: options[:description]}.compact
|
117
|
+
when :null
|
118
|
+
{type: "null", description: options[:description]}.compact
|
119
|
+
when :object
|
120
|
+
sub_schema = Class.new(Schema)
|
121
|
+
sub_schema.class_eval(&)
|
122
|
+
|
123
|
+
{
|
124
|
+
type: "object",
|
125
|
+
properties: sub_schema.properties,
|
126
|
+
required: sub_schema.required_properties,
|
127
|
+
additionalProperties: additional_properties,
|
128
|
+
description: options[:description]
|
129
|
+
}.compact
|
130
|
+
else
|
131
|
+
raise InvalidSchemaTypeError, type
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate!
|
136
|
+
validator = Validator.new(self)
|
137
|
+
validator.validate!
|
138
|
+
end
|
139
|
+
|
140
|
+
def valid?
|
141
|
+
validator = Validator.new(self)
|
142
|
+
validator.valid?
|
143
|
+
end
|
144
|
+
|
145
|
+
private
|
146
|
+
|
147
|
+
def add_property(name, definition, required:)
|
148
|
+
properties[name.to_sym] = definition
|
149
|
+
required_properties << name.to_sym if required
|
150
|
+
end
|
151
|
+
|
152
|
+
def determine_array_items(of, &)
|
153
|
+
return collect_property_schemas_from_block(&).first if block_given?
|
154
|
+
return build_property_schema(of) if primitive_type?(of)
|
155
|
+
return reference(of) if of.is_a?(Symbol)
|
156
|
+
|
157
|
+
raise InvalidArrayTypeError, of
|
158
|
+
end
|
159
|
+
|
160
|
+
def collect_property_schemas_from_block(&)
|
161
|
+
collector = PropertySchemaCollector.new
|
162
|
+
collector.collect(&)
|
163
|
+
collector.schemas
|
164
|
+
end
|
165
|
+
|
166
|
+
def primitive_type?(type)
|
167
|
+
type.is_a?(Symbol) && PRIMITIVE_TYPES.include?(type)
|
168
|
+
end
|
169
|
+
end
|
170
|
+
|
171
|
+
def initialize(name = nil, description: nil)
|
172
|
+
@name = name || self.class.name || "Schema"
|
173
|
+
@description = description
|
174
|
+
end
|
175
|
+
|
176
|
+
def to_json_schema
|
177
|
+
validate! # Validate schema before generating JSON
|
178
|
+
|
179
|
+
{
|
180
|
+
name: @name,
|
181
|
+
description: @description,
|
182
|
+
schema: {
|
183
|
+
:type => "object",
|
184
|
+
:properties => self.class.properties,
|
185
|
+
:required => self.class.required_properties,
|
186
|
+
:additionalProperties => self.class.additional_properties,
|
187
|
+
:strict => self.class.strict,
|
188
|
+
"$defs" => self.class.definitions
|
189
|
+
}
|
190
|
+
}
|
191
|
+
end
|
192
|
+
|
193
|
+
def to_json(*_args)
|
194
|
+
validate! # Validate schema before generating JSON string
|
195
|
+
JSON.pretty_generate(to_json_schema)
|
196
|
+
end
|
197
|
+
|
198
|
+
def validate!
|
199
|
+
self.class.validate!
|
200
|
+
end
|
201
|
+
|
202
|
+
def valid?
|
203
|
+
self.class.valid?
|
204
|
+
end
|
205
|
+
|
206
|
+
def method_missing(method_name, ...)
|
207
|
+
if respond_to_missing?(method_name)
|
208
|
+
send(method_name, ...)
|
209
|
+
else
|
210
|
+
super
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def respond_to_missing?(method_name, include_private = false)
|
215
|
+
%i[string number integer boolean array object any_of null].include?(method_name) || super
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
namespace :release do
|
2
|
+
desc "Release a new version of the gem"
|
3
|
+
task :version do
|
4
|
+
puts "Enter the new version: "
|
5
|
+
version = gets.chomp
|
6
|
+
system "git tag v#{version}"
|
7
|
+
system "git push origin v#{version}"
|
8
|
+
|
9
|
+
system "gem build ruby_llm-schema.gemspec"
|
10
|
+
system "gem push ruby_llm-schema-#{version}.gem"
|
11
|
+
system "rm ruby_llm-schema-#{version}.gem"
|
12
|
+
end
|
13
|
+
end
|
metadata
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby_llm-schema
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Daniel Friis
|
8
|
+
bindir: exe
|
9
|
+
cert_chain: []
|
10
|
+
date: 2025-06-16 00:00:00.000000000 Z
|
11
|
+
dependencies:
|
12
|
+
- !ruby/object:Gem::Dependency
|
13
|
+
name: rspec
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
15
|
+
requirements:
|
16
|
+
- - "~>"
|
17
|
+
- !ruby/object:Gem::Version
|
18
|
+
version: '3.0'
|
19
|
+
type: :development
|
20
|
+
prerelease: false
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
22
|
+
requirements:
|
23
|
+
- - "~>"
|
24
|
+
- !ruby/object:Gem::Version
|
25
|
+
version: '3.0'
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: standard
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
29
|
+
requirements:
|
30
|
+
- - ">="
|
31
|
+
- !ruby/object:Gem::Version
|
32
|
+
version: '0'
|
33
|
+
type: :development
|
34
|
+
prerelease: false
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
36
|
+
requirements:
|
37
|
+
- - ">="
|
38
|
+
- !ruby/object:Gem::Version
|
39
|
+
version: '0'
|
40
|
+
email:
|
41
|
+
- d@friis.me
|
42
|
+
executables: []
|
43
|
+
extensions: []
|
44
|
+
extra_rdoc_files: []
|
45
|
+
files:
|
46
|
+
- ".rspec"
|
47
|
+
- ".rspec_status"
|
48
|
+
- LICENSE.txt
|
49
|
+
- README.md
|
50
|
+
- Rakefile
|
51
|
+
- lib/ruby_llm/schema.rb
|
52
|
+
- lib/ruby_llm/schema/errors.rb
|
53
|
+
- lib/ruby_llm/schema/helpers.rb
|
54
|
+
- lib/ruby_llm/schema/property_schema_collector.rb
|
55
|
+
- lib/ruby_llm/schema/validator.rb
|
56
|
+
- lib/ruby_llm/schema/version.rb
|
57
|
+
- lib/tasks/release.rake
|
58
|
+
homepage: https://github.com/danielfriis/ruby_llm-schema
|
59
|
+
licenses:
|
60
|
+
- MIT
|
61
|
+
metadata:
|
62
|
+
homepage_uri: https://github.com/danielfriis/ruby_llm-schema
|
63
|
+
source_code_uri: https://github.com/danielfriis/ruby_llm-schema
|
64
|
+
changelog_uri: https://github.com/danielfriis/ruby_llm-schema/blob/main/CHANGELOG.md
|
65
|
+
rubygems_mfa_required: 'true'
|
66
|
+
rdoc_options: []
|
67
|
+
require_paths:
|
68
|
+
- lib
|
69
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
70
|
+
requirements:
|
71
|
+
- - ">="
|
72
|
+
- !ruby/object:Gem::Version
|
73
|
+
version: 3.1.0
|
74
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
version: '0'
|
79
|
+
requirements: []
|
80
|
+
rubygems_version: 3.6.2
|
81
|
+
specification_version: 4
|
82
|
+
summary: A simple and clean Ruby DSL for creating JSON schemas.
|
83
|
+
test_files: []
|