ruby_llm-schema 0.1.9 → 0.2.1

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: 5965c9d64040358651a74d313d1bd50010378ca33b20ec46a26db8fb006e4e25
4
- data.tar.gz: a4d9ac3040a6ce33fe0bbc4294166c00e8cafb5e87dcb336d47e1541b504dc12
3
+ metadata.gz: 51853d66ecd0a91e208955e1f3d1e5f4a61cecf8762e49d1b170ba74408b93b1
4
+ data.tar.gz: 05176acf10b4fbf638d486ed469126f352f3dc8c82ac7e4eea83f79bfe69d1c9
5
5
  SHA512:
6
- metadata.gz: d6ce13ba9d6164a794004fe1138f92ab8d141348d86b33163ad23c119425f252f57b585853ab73a21cff102de9cc403f8936f7b63d4b52d4a572795d8a340aad
7
- data.tar.gz: 4edbb7d38e7fa1414a812d0fa87f6f20b24e19d2ceb1a151e476c1b330d2e6ac84cfc7faf4aa388858d322bbdaeccffcaa5002db4d19e5a68d5e263876b3db0f
6
+ metadata.gz: 83e04d000fedfc6a532c6195416249c96ea1ae6511004d29cc8eca5cb9bba73854ec563a633236d4838669fe60a1e32ab879484883926cf7e2924eb709ba9c05
7
+ data.tar.gz: 2325e7f6b0b40841e65772c5ba92119ce752f33a15926b25d14348c6ceae016a59c6a4a718b4c5f79c928f145ebfcf384262ca9228878eb03db50e669abf3286
data/README.md CHANGED
@@ -262,7 +262,7 @@ end
262
262
 
263
263
  ### Schema Definitions and References
264
264
 
265
- You can define sub-schemas and reference them in other schemas.
265
+ You can define sub-schemas and reference them in other schemas, or reference the root schema to generate recursive schemas.
266
266
 
267
267
  ```ruby
268
268
  class MySchema < RubyLLM::Schema
@@ -281,6 +281,35 @@ class MySchema < RubyLLM::Schema
281
281
  object :user do
282
282
  reference :location
283
283
  end
284
+
285
+ # Using a reference to the root schema
286
+ object :ui_schema do
287
+ string :element, enum: ["input", "button"]
288
+ string :label
289
+ object :sub_schema, reference: :root
290
+ end
291
+ end
292
+ ```
293
+
294
+ ### Nested Schemas
295
+
296
+ You can embed existing schema classes directly within objects or arrays for reusable schema composition.
297
+
298
+ ```ruby
299
+ class PersonSchema < RubyLLM::Schema
300
+ string :name
301
+ integer :age
302
+ end
303
+
304
+ class CompanySchema < RubyLLM::Schema
305
+ # Using 'of' parameter
306
+ object :ceo, of: PersonSchema
307
+ array :employees, of: PersonSchema
308
+
309
+ # Using Schema.new in block
310
+ object :founder do
311
+ PersonSchema.new
312
+ end
284
313
  end
285
314
  ```
286
315
 
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Schema
5
+ module DSL
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)
9
+ end
10
+
11
+ def array(name, description: nil, required: true, **options, &block)
12
+ add_property(name, array_schema(description: description, **options, &block), required: required)
13
+ end
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)
17
+ end
18
+
19
+ def optional(name, description: nil, &block)
20
+ any_of(name, description: description) do
21
+ instance_eval(&block)
22
+ null
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Schema
5
+ module DSL
6
+ module PrimitiveTypes
7
+ def string(name, description: nil, required: true, **options)
8
+ add_property(name, string_schema(description: description, **options), required: required)
9
+ end
10
+
11
+ def number(name, description: nil, required: true, **options)
12
+ add_property(name, number_schema(description: description, **options), required: required)
13
+ end
14
+
15
+ def integer(name, description: nil, required: true, **options)
16
+ add_property(name, integer_schema(description: description, **options), required: required)
17
+ end
18
+
19
+ def boolean(name, description: nil, required: true, **options)
20
+ add_property(name, boolean_schema(description: description, **options), required: required)
21
+ end
22
+
23
+ def null(name, description: nil, required: true, **options)
24
+ add_property(name, null_schema(description: description, **options), required: required)
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,182 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Schema
5
+ module DSL
6
+ module SchemaBuilders
7
+ def string_schema(description: nil, enum: nil, min_length: nil, max_length: nil, pattern: nil, format: nil)
8
+ {
9
+ type: "string",
10
+ enum: enum,
11
+ description: description,
12
+ minLength: min_length,
13
+ maxLength: max_length,
14
+ pattern: pattern,
15
+ format: format
16
+ }.compact
17
+ end
18
+
19
+ def number_schema(description: nil, minimum: nil, maximum: nil, multiple_of: nil)
20
+ {
21
+ type: "number",
22
+ description: description,
23
+ minimum: minimum,
24
+ maximum: maximum,
25
+ multipleOf: multiple_of
26
+ }.compact
27
+ end
28
+
29
+ def integer_schema(description: nil, minimum: nil, maximum: nil, multiple_of: nil)
30
+ {
31
+ type: "integer",
32
+ description: description,
33
+ minimum: minimum,
34
+ maximum: maximum,
35
+ multipleOf: multiple_of
36
+ }.compact
37
+ end
38
+
39
+ def boolean_schema(description: nil)
40
+ {type: "boolean", description: description}.compact
41
+ end
42
+
43
+ def null_schema(description: nil)
44
+ {type: "null", description: description}.compact
45
+ end
46
+
47
+ def object_schema(description: nil, of: nil, reference: nil, &block)
48
+ if reference
49
+ warn "[DEPRECATION] The `reference` option will be deprecated. Please use `of` instead."
50
+ of = reference
51
+ end
52
+
53
+ if of
54
+ determine_object_reference(of, description)
55
+ else
56
+ sub_schema = Class.new(Schema)
57
+ result = sub_schema.class_eval(&block)
58
+
59
+ # If the block returned a reference and no properties were added, use the reference
60
+ if result.is_a?(Hash) && result["$ref"] && sub_schema.properties.empty?
61
+ result.merge(description ? {description: description} : {})
62
+ # If the block returned a Schema class or instance, convert it to inline schema
63
+ elsif schema_class?(result) && sub_schema.properties.empty?
64
+ schema_class_to_inline_schema(result).merge(description ? {description: description} : {})
65
+ # Block didn't return reference or schema, so we build an inline object schema
66
+ else
67
+ {
68
+ type: "object",
69
+ properties: sub_schema.properties,
70
+ required: sub_schema.required_properties,
71
+ additionalProperties: sub_schema.additional_properties,
72
+ description: description
73
+ }.compact
74
+ end
75
+ end
76
+ end
77
+
78
+ def array_schema(description: nil, of: nil, min_items: nil, max_items: nil, &block)
79
+ items = determine_array_items(of, &block)
80
+
81
+ {
82
+ type: "array",
83
+ description: description,
84
+ items: items,
85
+ minItems: min_items,
86
+ maxItems: max_items
87
+ }.compact
88
+ end
89
+
90
+ def any_of_schema(description: nil, &block)
91
+ schemas = collect_schemas_from_block(&block)
92
+
93
+ {
94
+ description: description,
95
+ anyOf: schemas
96
+ }.compact
97
+ end
98
+
99
+ private
100
+
101
+ def determine_array_items(of, &)
102
+ return collect_schemas_from_block(&).first if block_given?
103
+ return send("#{of}_schema") if primitive_type?(of)
104
+ return reference(of) if of.is_a?(Symbol)
105
+ return schema_class_to_inline_schema(of) if schema_class?(of)
106
+
107
+ raise InvalidArrayTypeError, "Invalid array type: #{of.inspect}. Must be a primitive type (:string, :number, etc.), a symbol reference, a Schema class, or a Schema instance."
108
+ end
109
+
110
+ def determine_object_reference(of, description = nil)
111
+ result = case of
112
+ when Symbol
113
+ reference(of)
114
+ when Class
115
+ if schema_class?(of)
116
+ schema_class_to_inline_schema(of)
117
+ else
118
+ raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Class must inherit from RubyLLM::Schema."
119
+ end
120
+ else
121
+ if schema_class?(of)
122
+ schema_class_to_inline_schema(of)
123
+ else
124
+ raise InvalidObjectTypeError, "Invalid object type: #{of.inspect}. Must be a symbol reference, a Schema class, or a Schema instance."
125
+ end
126
+ end
127
+
128
+ description ? result.merge(description: description) : result
129
+ end
130
+
131
+ def collect_schemas_from_block(&block)
132
+ schemas = []
133
+ schema_builder = self
134
+
135
+ context = Object.new
136
+
137
+ # Dynamically create methods for all schema builders
138
+ schema_builder.methods.grep(/_schema$/).each do |schema_method|
139
+ type_name = schema_method.to_s.sub(/_schema$/, "")
140
+
141
+ context.define_singleton_method(type_name) do |name = nil, **options, &blk|
142
+ schemas << schema_builder.send(schema_method, **options, &blk)
143
+ end
144
+ end
145
+
146
+ # Allow Schema classes to be accessed in the context
147
+ context.define_singleton_method(:const_missing) do |name|
148
+ const_get(name) if const_defined?(name)
149
+ end
150
+
151
+ context.instance_eval(&block)
152
+ schemas
153
+ end
154
+
155
+ def schema_class_to_inline_schema(schema_class_or_instance)
156
+ # Handle both Schema classes and Schema instances
157
+ schema_class = if schema_class_or_instance.is_a?(Class)
158
+ schema_class_or_instance
159
+ else
160
+ schema_class_or_instance.class
161
+ end
162
+
163
+ # Directly convert schema class to inline object schema
164
+ {
165
+ type: "object",
166
+ properties: schema_class.properties,
167
+ required: schema_class.required_properties,
168
+ additionalProperties: schema_class.additional_properties
169
+ }.tap do |schema|
170
+ # For instances, prefer instance description over class description
171
+ description = if schema_class_or_instance.is_a?(Class)
172
+ schema_class.description
173
+ else
174
+ schema_class_or_instance.instance_variable_get(:@description) || schema_class.description
175
+ end
176
+ schema[:description] = description if description
177
+ end
178
+ end
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyLLM
4
+ class Schema
5
+ module DSL
6
+ module Utilities
7
+ # Schema definition and reference methods
8
+ def define(name, &)
9
+ sub_schema = Class.new(Schema)
10
+ sub_schema.class_eval(&)
11
+
12
+ definitions[name] = {
13
+ type: "object",
14
+ properties: sub_schema.properties,
15
+ required: sub_schema.required_properties,
16
+ additionalProperties: sub_schema.additional_properties
17
+ }
18
+ end
19
+
20
+ def reference(schema_name)
21
+ if schema_name == :root
22
+ {"$ref" => "#"}
23
+ else
24
+ {"$ref" => "#/$defs/#{schema_name}"}
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def add_property(name, definition, required:)
31
+ properties[name.to_sym] = definition
32
+ required_properties << name.to_sym if required
33
+ end
34
+
35
+ def primitive_type?(type)
36
+ type.is_a?(Symbol) && PRIMITIVE_TYPES.include?(type)
37
+ end
38
+
39
+ def schema_class?(type)
40
+ (type.is_a?(Class) && type < Schema) || type.is_a?(Schema)
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -1,194 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "dsl/schema_builders"
4
+ require_relative "dsl/primitive_types"
5
+ require_relative "dsl/complex_types"
6
+ require_relative "dsl/utilities"
7
+
3
8
  module RubyLLM
4
9
  class Schema
5
10
  module DSL
6
- # Primitive type methods
7
- def string(name = nil, enum: nil, description: nil, required: true, min_length: nil, max_length: nil, pattern: nil, format: nil)
8
- options = {
9
- enum: enum,
10
- description: description,
11
- minLength: min_length,
12
- maxLength: max_length,
13
- pattern: pattern,
14
- format: format
15
- }.compact
16
-
17
- add_property(name, build_property_schema(:string, **options), required: required)
18
- end
19
-
20
- def number(name = nil, description: nil, required: true, minimum: nil, maximum: nil, multiple_of: nil)
21
- options = {
22
- description: description,
23
- minimum: minimum,
24
- maximum: maximum,
25
- multipleOf: multiple_of
26
- }.compact
27
-
28
- add_property(name, build_property_schema(:number, **options), required: required)
29
- end
30
-
31
- def integer(name = nil, description: nil, required: true)
32
- add_property(name, build_property_schema(:integer, description: description), required: required)
33
- end
34
-
35
- def boolean(name = nil, description: nil, required: true)
36
- add_property(name, build_property_schema(:boolean, description: description), required: required)
37
- end
38
-
39
- def null(name = nil, description: nil, required: true)
40
- add_property(name, build_property_schema(:null, description: description), required: required)
41
- end
42
-
43
- # Complex type methods
44
- def object(name = nil, reference: nil, description: nil, required: true, &block)
45
- add_property(name, build_property_schema(:object, description: description, reference: reference, &block), required: required)
46
- end
47
-
48
- def array(name, of: nil, description: nil, required: true, min_items: nil, max_items: nil, &block)
49
- items = determine_array_items(of, &block)
50
-
51
- add_property(name, {
52
- type: "array",
53
- description: description,
54
- items: items,
55
- minItems: min_items,
56
- maxItems: max_items
57
- }.compact, required: required)
58
- end
59
-
60
- def any_of(name = nil, required: true, description: nil, &block)
61
- schemas = collect_property_schemas_from_block(&block)
62
-
63
- add_property(name, {
64
- description: description,
65
- anyOf: schemas
66
- }.compact, required: required)
67
- end
68
-
69
- def optional(name, description: nil, &block)
70
- any_of(name, description: description) do
71
- instance_eval(&block)
72
- null
73
- end
74
- end
75
-
76
- # Schema definition and reference methods
77
- def define(name, &)
78
- sub_schema = Class.new(Schema)
79
- sub_schema.class_eval(&)
80
-
81
- definitions[name] = {
82
- type: "object",
83
- properties: sub_schema.properties,
84
- required: sub_schema.required_properties,
85
- additionalProperties: sub_schema.additional_properties
86
- }
87
- end
88
-
89
- def reference(schema_name)
90
- {"$ref" => "#/$defs/#{schema_name}"}
91
- end
92
-
93
- # Schema building methods
94
- def build_property_schema(type, **options, &)
95
- case type
96
- when :string
97
- {
98
- type: "string",
99
- enum: options[:enum],
100
- description: options[:description],
101
- minLength: options[:minLength],
102
- maxLength: options[:maxLength],
103
- pattern: options[:pattern],
104
- format: options[:format]
105
- }.compact
106
- when :number
107
- {
108
- type: "number",
109
- description: options[:description],
110
- minimum: options[:minimum],
111
- maximum: options[:maximum],
112
- multipleOf: options[:multipleOf]
113
- }.compact
114
- when :integer
115
- {
116
- type: "integer",
117
- description: options[:description],
118
- minimum: options[:minimum],
119
- maximum: options[:maximum],
120
- multipleOf: options[:multipleOf]
121
- }.compact
122
- when :boolean
123
- {type: "boolean", description: options[:description]}.compact
124
- when :null
125
- {type: "null", description: options[:description]}.compact
126
- when :object
127
- # If the reference option is provided, return the reference
128
- return reference(options[:reference]) if options[:reference]
129
-
130
- sub_schema = Class.new(Schema)
131
-
132
- # Evaluate the block and capture the result
133
- result = sub_schema.class_eval(&)
134
-
135
- # If the block returned a reference and no properties were added, use the reference
136
- if result.is_a?(Hash) && result["$ref"] && sub_schema.properties.empty?
137
- result.merge(options[:description] ? {description: options[:description]} : {})
138
- else
139
- {
140
- type: "object",
141
- properties: sub_schema.properties,
142
- required: sub_schema.required_properties,
143
- additionalProperties: sub_schema.additional_properties,
144
- description: options[:description]
145
- }.compact
146
- end
147
- when :any_of
148
- schemas = collect_property_schemas_from_block(&)
149
- {
150
- anyOf: schemas
151
- }.compact
152
- else
153
- raise InvalidSchemaTypeError, type
154
- end
155
- end
156
-
157
- private
158
-
159
- def add_property(name, definition, required:)
160
- properties[name.to_sym] = definition
161
- required_properties << name.to_sym if required
162
- end
163
-
164
- def determine_array_items(of, &)
165
- return collect_property_schemas_from_block(&).first if block_given?
166
- return build_property_schema(of) if primitive_type?(of)
167
- return reference(of) if of.is_a?(Symbol)
168
-
169
- raise InvalidArrayTypeError, of
170
- end
171
-
172
- def collect_property_schemas_from_block(&block)
173
- schemas = []
174
- schema_builder = self # Capture the current context that has build_property_schema
175
-
176
- context = Object.new
177
- context.define_singleton_method(:string) { |name = nil, **options| schemas << schema_builder.build_property_schema(:string, **options) }
178
- context.define_singleton_method(:number) { |name = nil, **options| schemas << schema_builder.build_property_schema(:number, **options) }
179
- context.define_singleton_method(:integer) { |name = nil, **options| schemas << schema_builder.build_property_schema(:integer, **options) }
180
- context.define_singleton_method(:boolean) { |name = nil, **options| schemas << schema_builder.build_property_schema(:boolean, **options) }
181
- context.define_singleton_method(:null) { |name = nil, **options| schemas << schema_builder.build_property_schema(:null, **options) }
182
- context.define_singleton_method(:object) { |name = nil, **options, &blk| schemas << schema_builder.build_property_schema(:object, **options, &blk) }
183
- context.define_singleton_method(:any_of) { |name = nil, **options, &blk| schemas << schema_builder.build_property_schema(:any_of, **options, &blk) }
184
-
185
- context.instance_eval(&block)
186
- schemas
187
- end
188
-
189
- def primitive_type?(type)
190
- type.is_a?(Symbol) && PRIMITIVE_TYPES.include?(type)
191
- end
11
+ include SchemaBuilders
12
+ include PrimitiveTypes
13
+ include ComplexTypes
14
+ include Utilities
192
15
  end
193
16
  end
194
17
  end
@@ -14,8 +14,15 @@ module RubyLLM
14
14
 
15
15
  # Raised when an invalid array type is specified
16
16
  class InvalidArrayTypeError < Error
17
- def initialize(type)
18
- super("Invalid array type: #{type}")
17
+ def initialize(message)
18
+ super
19
+ end
20
+ end
21
+
22
+ # Raised when an invalid object type is specified
23
+ class InvalidObjectTypeError < Error
24
+ def initialize(message)
25
+ super
19
26
  end
20
27
  end
21
28
 
@@ -6,17 +6,21 @@ module RubyLLM
6
6
  def to_json_schema
7
7
  validate! # Validate schema before generating JSON
8
8
 
9
+ schema_hash = {
10
+ type: "object",
11
+ properties: self.class.properties,
12
+ required: self.class.required_properties,
13
+ additionalProperties: self.class.additional_properties,
14
+ strict: self.class.strict
15
+ }
16
+
17
+ # Only include $defs if there are definitions
18
+ schema_hash["$defs"] = self.class.definitions unless self.class.definitions.empty?
19
+
9
20
  {
10
21
  name: @name,
11
22
  description: @description || self.class.description,
12
- schema: {
13
- :type => "object",
14
- :properties => self.class.properties,
15
- :required => self.class.required_properties,
16
- :additionalProperties => self.class.additional_properties,
17
- :strict => self.class.strict,
18
- "$defs" => self.class.definitions
19
- }
23
+ schema: schema_hash
20
24
  }
21
25
  end
22
26
 
@@ -2,6 +2,6 @@
2
2
 
3
3
  module RubyLlm
4
4
  class Schema
5
- VERSION = "0.1.9"
5
+ VERSION = "0.2.1"
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby_llm-schema
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.9
4
+ version: 0.2.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Daniel Friis
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2025-08-22 00:00:00.000000000 Z
10
+ date: 2025-08-26 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: rspec
@@ -49,6 +49,10 @@ files:
49
49
  - Rakefile
50
50
  - lib/ruby_llm/schema.rb
51
51
  - lib/ruby_llm/schema/dsl.rb
52
+ - lib/ruby_llm/schema/dsl/complex_types.rb
53
+ - lib/ruby_llm/schema/dsl/primitive_types.rb
54
+ - lib/ruby_llm/schema/dsl/schema_builders.rb
55
+ - lib/ruby_llm/schema/dsl/utilities.rb
52
56
  - lib/ruby_llm/schema/errors.rb
53
57
  - lib/ruby_llm/schema/helpers.rb
54
58
  - lib/ruby_llm/schema/json_output.rb