ruby_llm-schema 0.3.1 → 0.4.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/README.md +62 -2
- data/lib/ruby_llm/schema/dsl/complex_types.rb +8 -8
- data/lib/ruby_llm/schema/dsl/conditionals.rb +169 -0
- data/lib/ruby_llm/schema/dsl/primitive_types.rb +10 -10
- data/lib/ruby_llm/schema/dsl/schema_builders.rb +6 -1
- data/lib/ruby_llm/schema/dsl/utilities.rb +12 -2
- data/lib/ruby_llm/schema/dsl.rb +2 -0
- data/lib/ruby_llm/schema/json_output.rb +2 -0
- data/lib/ruby_llm/schema/version.rb +1 -1
- metadata +2 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21c3e0ee2e5a1034eb571749cc2f27af5db9e4abaf07e2df677325f9270db6f1
|
|
4
|
+
data.tar.gz: 57db5c67a751f4e31f1ed2d56bba8bac76ddb989fee7b49add969b1c50be844e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: cd93b24110859806ee43a5a794353849e51273797696d84469b998c69d9e93e761ef817ede4d89377e34e227f8b6115d2e9133d58496bfcb2399f7720e4087e3
|
|
7
|
+
data.tar.gz: 1153950f2917a95e00744cb832a2c3ce7b1bd61965314f5652dc4337333d3b9f258d52b01257e62739b997bbd6ede840c9a7adfcd339477dc996bca093bf2a0d
|
data/README.md
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
# RubyLLM::Schema
|
|
2
2
|
|
|
3
3
|
[](https://rubygems.org/gems/ruby_llm-schema)
|
|
4
|
-
[](https://rubygems.org/gems/ruby_llm-schema)
|
|
5
|
+
[](https://codecov.io/gh/crmne/ruby_llm-schema)
|
|
6
|
+
[](https://github.com/rubocop/rubocop)
|
|
6
7
|
|
|
7
8
|
A Ruby DSL for creating JSON schemas with a clean, Rails-inspired API.
|
|
8
9
|
|
|
@@ -443,6 +444,65 @@ schema.to_json_schema
|
|
|
443
444
|
# }
|
|
444
445
|
```
|
|
445
446
|
|
|
447
|
+
### Dependencies
|
|
448
|
+
|
|
449
|
+
Use `requires:` inline or `dependent` block to express that the presence of one property requires others. Maps to [`dependentRequired`](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentRequired) (Draft 2019-09) and [`dependentSchemas`](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentSchemas) (Draft 2019-09). Check your provider's documentation for compatibility.
|
|
450
|
+
|
|
451
|
+
```ruby
|
|
452
|
+
class PaymentSchema < RubyLLM::Schema
|
|
453
|
+
string :name
|
|
454
|
+
number :credit_card, required: false, requires: %i[billing_address cvv]
|
|
455
|
+
string :billing_address, required: false
|
|
456
|
+
string :cvv, required: false
|
|
457
|
+
end
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
Use a `dependent` block when you also need validations — this upgrades the output to `dependentSchemas`:
|
|
461
|
+
|
|
462
|
+
```ruby
|
|
463
|
+
dependent :credit_card do
|
|
464
|
+
requires :billing_address
|
|
465
|
+
validates :billing_address, type: :string, min_length: 1
|
|
466
|
+
end
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
### Conditionals
|
|
470
|
+
|
|
471
|
+
Use `given` to add [JSON Schema `if`/`then`/`else`](https://json-schema.org/understanding-json-schema/reference/conditionals#ifthenelse) (Draft 7) rules. Condition values are automatically coerced: strings → `const`, arrays → `enum`, regexps → `pattern`, hashes → raw schema.
|
|
472
|
+
|
|
473
|
+
```ruby
|
|
474
|
+
class OrderSchema < RubyLLM::Schema
|
|
475
|
+
string :status, enum: ["pending", "shipped", "cancelled"]
|
|
476
|
+
string :tracking_number, required: false
|
|
477
|
+
string :cancellation_reason, required: false
|
|
478
|
+
|
|
479
|
+
given status: "shipped" do
|
|
480
|
+
requires :tracking_number
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
given status: "cancelled" do
|
|
484
|
+
requires :cancellation_reason
|
|
485
|
+
validates :cancellation_reason, type: :string, min_length: 1
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
```
|
|
489
|
+
|
|
490
|
+
`validates` supports: `type:`, `not_value:`, `min_length:`, `max_length:`, `pattern:` (string or regexp), `enum:`, `const:`, `minimum:`, `maximum:`.
|
|
491
|
+
|
|
492
|
+
Use `otherwise` for an `else` branch:
|
|
493
|
+
|
|
494
|
+
```ruby
|
|
495
|
+
given domestic: true do
|
|
496
|
+
requires :state
|
|
497
|
+
|
|
498
|
+
otherwise do
|
|
499
|
+
requires :country
|
|
500
|
+
end
|
|
501
|
+
end
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
Conditions propagate through nested schemas via `of:`.
|
|
505
|
+
|
|
446
506
|
## JSON Output
|
|
447
507
|
|
|
448
508
|
```ruby
|
|
@@ -4,20 +4,20 @@ module RubyLLM
|
|
|
4
4
|
class Schema
|
|
5
5
|
module DSL
|
|
6
6
|
module ComplexTypes
|
|
7
|
-
def object(name, description: nil, required: true, **options, &block)
|
|
8
|
-
add_property(name, object_schema(description: description, **options, &block), required: required)
|
|
7
|
+
def object(name, description: nil, required: true, requires: nil, **options, &block)
|
|
8
|
+
add_property(name, object_schema(description: description, **options, &block), required: required, requires: requires)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def array(name, description: nil, required: true, **options, &block)
|
|
12
|
-
add_property(name, array_schema(description: description, **options, &block), required: required)
|
|
11
|
+
def array(name, description: nil, required: true, requires: nil, **options, &block)
|
|
12
|
+
add_property(name, array_schema(description: description, **options, &block), required: required, requires: requires)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def any_of(name, description: nil, required: true, **options, &block)
|
|
16
|
-
add_property(name, any_of_schema(description: description, **options, &block), required: required)
|
|
15
|
+
def any_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
16
|
+
add_property(name, any_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def one_of(name, description: nil, required: true, **options, &block)
|
|
20
|
-
add_property(name, one_of_schema(description: description, **options, &block), required: required)
|
|
19
|
+
def one_of(name, description: nil, required: true, requires: nil, **options, &block)
|
|
20
|
+
add_property(name, one_of_schema(description: description, **options, &block), required: required, requires: requires)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
23
|
def optional(name, description: nil, &block)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RubyLLM
|
|
4
|
+
class Schema
|
|
5
|
+
module DSL
|
|
6
|
+
module Conditionals
|
|
7
|
+
def conditions
|
|
8
|
+
@conditions ||= []
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def dependencies
|
|
12
|
+
@dependencies ||= {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def dependent(property, &block)
|
|
16
|
+
builder = ConditionalBuilder.new
|
|
17
|
+
builder.instance_eval(&block)
|
|
18
|
+
|
|
19
|
+
dependencies[property.to_s] = builder
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def given(**properties, &block)
|
|
23
|
+
raise ArgumentError, "given requires at least one property condition" if properties.empty?
|
|
24
|
+
|
|
25
|
+
if_schema = {
|
|
26
|
+
properties: properties.transform_keys(&:to_s).transform_values { |v| coerce_condition(v) },
|
|
27
|
+
required: properties.keys.map(&:to_s)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
then_builder = ConditionalBuilder.new
|
|
31
|
+
else_builder = ConditionalBuilder.new
|
|
32
|
+
|
|
33
|
+
context = ConditionalContext.new(then_builder, else_builder)
|
|
34
|
+
context.instance_eval(&block)
|
|
35
|
+
|
|
36
|
+
condition = {if: if_schema, then: then_builder.to_schema}
|
|
37
|
+
condition[:else] = else_builder.to_schema unless else_builder.empty?
|
|
38
|
+
|
|
39
|
+
conditions << condition
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
private
|
|
43
|
+
|
|
44
|
+
def merge_conditions(schema, schema_class)
|
|
45
|
+
if schema_class.respond_to?(:conditions) && schema_class.conditions.any?
|
|
46
|
+
if schema_class.conditions.length == 1
|
|
47
|
+
schema.merge!(schema_class.conditions.first)
|
|
48
|
+
else
|
|
49
|
+
schema[:allOf] = schema_class.conditions
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
if schema_class.respond_to?(:dependencies) && schema_class.dependencies.any?
|
|
54
|
+
dependent_required = {}
|
|
55
|
+
dependent_schemas = {}
|
|
56
|
+
|
|
57
|
+
schema_class.dependencies.each do |property, builder|
|
|
58
|
+
if builder.validations_empty?
|
|
59
|
+
dependent_required[property] = builder.required_fields
|
|
60
|
+
else
|
|
61
|
+
dependent_schemas[property] = builder.to_schema
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
schema[:dependentRequired] = dependent_required if dependent_required.any?
|
|
66
|
+
schema[:dependentSchemas] = dependent_schemas if dependent_schemas.any?
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
schema
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def coerce_condition(value)
|
|
73
|
+
case value
|
|
74
|
+
when Array then {enum: value}
|
|
75
|
+
when Regexp then {pattern: value.source}
|
|
76
|
+
when Hash then value
|
|
77
|
+
else {const: value}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
class ConditionalContext
|
|
83
|
+
def initialize(then_builder, else_builder)
|
|
84
|
+
@then_builder = then_builder
|
|
85
|
+
@else_builder = else_builder
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def requires(*fields)
|
|
89
|
+
@then_builder.requires(*fields)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validates(field, **options)
|
|
93
|
+
@then_builder.validates(field, **options)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def otherwise(&block)
|
|
97
|
+
@else_builder.instance_eval(&block)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
class ConditionalBuilder
|
|
102
|
+
def requires(*fields)
|
|
103
|
+
required.concat(fields.map(&:to_s))
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
VALIDATES_KEY_MAP = {
|
|
107
|
+
type: :type,
|
|
108
|
+
const: :const,
|
|
109
|
+
enum: :enum,
|
|
110
|
+
not_value: :not,
|
|
111
|
+
min_length: :minLength,
|
|
112
|
+
max_length: :maxLength,
|
|
113
|
+
pattern: :pattern,
|
|
114
|
+
minimum: :minimum,
|
|
115
|
+
maximum: :maximum
|
|
116
|
+
}.freeze
|
|
117
|
+
|
|
118
|
+
def validates(field, **options)
|
|
119
|
+
constraints = {}
|
|
120
|
+
|
|
121
|
+
options.each do |key, value|
|
|
122
|
+
schema_key = VALIDATES_KEY_MAP[key]
|
|
123
|
+
raise ArgumentError, "unknown validates option: #{key.inspect}" unless schema_key
|
|
124
|
+
|
|
125
|
+
case key
|
|
126
|
+
when :type then constraints[:type] = value.to_s
|
|
127
|
+
when :not_value then constraints[:not] = {const: value}
|
|
128
|
+
when :pattern then constraints[:pattern] = value.is_a?(Regexp) ? value.source : value
|
|
129
|
+
else constraints[schema_key] = value
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
validations[field.to_s] = constraints
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def to_schema
|
|
137
|
+
schema = {}
|
|
138
|
+
|
|
139
|
+
schema[:required] = required if required.any?
|
|
140
|
+
schema[:properties] = validations if validations.any?
|
|
141
|
+
|
|
142
|
+
schema
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def empty?
|
|
146
|
+
required.empty? && validations.empty?
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def required_fields
|
|
150
|
+
required.dup
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def validations_empty?
|
|
154
|
+
validations.empty?
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
private
|
|
158
|
+
|
|
159
|
+
def required
|
|
160
|
+
@required ||= []
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def validations
|
|
164
|
+
@validations ||= {}
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
@@ -4,24 +4,24 @@ module RubyLLM
|
|
|
4
4
|
class Schema
|
|
5
5
|
module DSL
|
|
6
6
|
module PrimitiveTypes
|
|
7
|
-
def string(name, description: nil, required: true, **options)
|
|
8
|
-
add_property(name, string_schema(description: description, **options), required: required)
|
|
7
|
+
def string(name, description: nil, required: true, requires: nil, **options)
|
|
8
|
+
add_property(name, string_schema(description: description, **options), required: required, requires: requires)
|
|
9
9
|
end
|
|
10
10
|
|
|
11
|
-
def number(name, description: nil, required: true, **options)
|
|
12
|
-
add_property(name, number_schema(description: description, **options), required: required)
|
|
11
|
+
def number(name, description: nil, required: true, requires: nil, **options)
|
|
12
|
+
add_property(name, number_schema(description: description, **options), required: required, requires: requires)
|
|
13
13
|
end
|
|
14
14
|
|
|
15
|
-
def integer(name, description: nil, required: true, **options)
|
|
16
|
-
add_property(name, integer_schema(description: description, **options), required: required)
|
|
15
|
+
def integer(name, description: nil, required: true, requires: nil, **options)
|
|
16
|
+
add_property(name, integer_schema(description: description, **options), required: required, requires: requires)
|
|
17
17
|
end
|
|
18
18
|
|
|
19
|
-
def boolean(name, description: nil, required: true, **options)
|
|
20
|
-
add_property(name, boolean_schema(description: description, **options), required: required)
|
|
19
|
+
def boolean(name, description: nil, required: true, requires: nil, **options)
|
|
20
|
+
add_property(name, boolean_schema(description: description, **options), required: required, requires: requires)
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
-
def null(name, description: nil, required: true, **options)
|
|
24
|
-
add_property(name, null_schema(description: description, **options), required: required)
|
|
23
|
+
def null(name, description: nil, required: true, requires: nil, **options)
|
|
24
|
+
add_property(name, null_schema(description: description, **options), required: required, requires: requires)
|
|
25
25
|
end
|
|
26
26
|
end
|
|
27
27
|
end
|
|
@@ -64,13 +64,15 @@ module RubyLLM
|
|
|
64
64
|
schema_class_to_inline_schema(result).merge(description ? {description: description} : {})
|
|
65
65
|
# Block didn't return reference or schema, so we build an inline object schema
|
|
66
66
|
else
|
|
67
|
-
{
|
|
67
|
+
schema = {
|
|
68
68
|
type: "object",
|
|
69
69
|
properties: sub_schema.properties,
|
|
70
70
|
required: sub_schema.required_properties,
|
|
71
71
|
additionalProperties: sub_schema.additional_properties,
|
|
72
72
|
description: description
|
|
73
73
|
}.compact
|
|
74
|
+
|
|
75
|
+
merge_conditions(schema, sub_schema)
|
|
74
76
|
end
|
|
75
77
|
end
|
|
76
78
|
end
|
|
@@ -180,7 +182,10 @@ module RubyLLM
|
|
|
180
182
|
else
|
|
181
183
|
schema_class_or_instance.instance_variable_get(:@description) || schema_class.description
|
|
182
184
|
end
|
|
185
|
+
|
|
183
186
|
schema[:description] = description if description
|
|
187
|
+
|
|
188
|
+
merge_conditions(schema, schema_class)
|
|
184
189
|
end
|
|
185
190
|
end
|
|
186
191
|
end
|
|
@@ -9,12 +9,16 @@ module RubyLLM
|
|
|
9
9
|
sub_schema = Class.new(Schema)
|
|
10
10
|
sub_schema.class_eval(&)
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
schema = {
|
|
13
13
|
type: "object",
|
|
14
14
|
properties: sub_schema.properties,
|
|
15
15
|
required: sub_schema.required_properties,
|
|
16
16
|
additionalProperties: sub_schema.additional_properties
|
|
17
17
|
}
|
|
18
|
+
|
|
19
|
+
merge_conditions(schema, sub_schema)
|
|
20
|
+
|
|
21
|
+
definitions[name] = schema
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
def reference(schema_name)
|
|
@@ -27,7 +31,7 @@ module RubyLLM
|
|
|
27
31
|
|
|
28
32
|
private
|
|
29
33
|
|
|
30
|
-
def add_property(name, definition, required:)
|
|
34
|
+
def add_property(name, definition, required:, requires: nil)
|
|
31
35
|
property_name = name.to_sym
|
|
32
36
|
|
|
33
37
|
properties[property_name] = definition
|
|
@@ -37,6 +41,12 @@ module RubyLLM
|
|
|
37
41
|
required_properties.delete(property_name)
|
|
38
42
|
end
|
|
39
43
|
|
|
44
|
+
if requires
|
|
45
|
+
builder = ConditionalBuilder.new
|
|
46
|
+
builder.requires(*Array(requires))
|
|
47
|
+
dependencies[name.to_s] = builder
|
|
48
|
+
end
|
|
49
|
+
|
|
40
50
|
nil
|
|
41
51
|
end
|
|
42
52
|
|
data/lib/ruby_llm/schema/dsl.rb
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "dsl/schema_builders"
|
|
4
4
|
require_relative "dsl/primitive_types"
|
|
5
5
|
require_relative "dsl/complex_types"
|
|
6
|
+
require_relative "dsl/conditionals"
|
|
6
7
|
require_relative "dsl/utilities"
|
|
7
8
|
|
|
8
9
|
module RubyLLM
|
|
@@ -11,6 +12,7 @@ module RubyLLM
|
|
|
11
12
|
include SchemaBuilders
|
|
12
13
|
include PrimitiveTypes
|
|
13
14
|
include ComplexTypes
|
|
15
|
+
include Conditionals
|
|
14
16
|
include Utilities
|
|
15
17
|
end
|
|
16
18
|
end
|
|
@@ -18,6 +18,8 @@ module RubyLLM
|
|
|
18
18
|
# Only include $defs if there are definitions
|
|
19
19
|
schema_hash["$defs"] = self.class.definitions unless self.class.definitions.empty?
|
|
20
20
|
|
|
21
|
+
self.class.send(:merge_conditions, schema_hash, self.class)
|
|
22
|
+
|
|
21
23
|
{
|
|
22
24
|
name: @name,
|
|
23
25
|
description: @description || self.class.description,
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: ruby_llm-schema
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.4.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Daniel Friis
|
|
@@ -22,6 +22,7 @@ files:
|
|
|
22
22
|
- lib/ruby_llm/schema.rb
|
|
23
23
|
- lib/ruby_llm/schema/dsl.rb
|
|
24
24
|
- lib/ruby_llm/schema/dsl/complex_types.rb
|
|
25
|
+
- lib/ruby_llm/schema/dsl/conditionals.rb
|
|
25
26
|
- lib/ruby_llm/schema/dsl/primitive_types.rb
|
|
26
27
|
- lib/ruby_llm/schema/dsl/schema_builders.rb
|
|
27
28
|
- lib/ruby_llm/schema/dsl/utilities.rb
|