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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fceccc2292efb7c17ce5457a08d6a18ddc3debd920052a1d8e368d74e450547e
4
- data.tar.gz: b16fafc541af3b9f1a1e3a96f81be21175a380606f2e40325afa29d76108daea
3
+ metadata.gz: 21c3e0ee2e5a1034eb571749cc2f27af5db9e4abaf07e2df677325f9270db6f1
4
+ data.tar.gz: 57db5c67a751f4e31f1ed2d56bba8bac76ddb989fee7b49add969b1c50be844e
5
5
  SHA512:
6
- metadata.gz: 984b11c9fcfcaf68f0892a9a7b9c10064436aa59279f0a0992ebae5c6d20fbcfd1e6afbef8376bf7a9239aaaea246dd9f8c7f6a3d9521509789c8e334e3e0363
7
- data.tar.gz: e9658cff5e3e1f0912eab47fe8438a5dfd5bf52928dd0198ff39131ecf1aeb5d3b2a8e08344feab0b363ed6a9a27747c1489fcfadefd8f30af4d03c77fb601d8
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
  [![Gem Version](https://badge.fury.io/rb/ruby_llm-schema.svg)](https://rubygems.org/gems/ruby_llm-schema)
4
- [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/crmne/ruby_llm-schema/blob/main/LICENSE)
5
- [![CI](https://github.com/crmne/ruby_llm-schema/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/crmne/ruby_llm-schema/actions/workflows/main.yml)
4
+ [![Gem Downloads](https://img.shields.io/gem/dt/ruby_llm-schema)](https://rubygems.org/gems/ruby_llm-schema)
5
+ [![codecov](https://codecov.io/gh/crmne/ruby_llm-schema/branch/main/graph/badge.svg)](https://codecov.io/gh/crmne/ruby_llm-schema)
6
+ [![Ruby Style Guide](https://img.shields.io/badge/code_style-rubocop-brightgreen.svg)](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
- definitions[name] = {
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
 
@@ -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,
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLLM
4
4
  class Schema
5
- VERSION = '0.3.1'
5
+ VERSION = '0.4.0'
6
6
  end
7
7
  end
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.3.1
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