From 7ab8ada2eb5c9f8c5ccb2d4f8dcbbc5472f0fdae Mon Sep 17 00:00:00 2001 From: Carmine Paolino Date: Sat, 6 Jun 2026 12:28:38 +0200 Subject: [PATCH] Add const validation keyword --- lib/ruby_llm/schema/dsl/schema_builders.rb | 51 ++++++++++++++----- .../schema/properties/booleans_spec.rb | 7 +++ spec/ruby_llm/schema/properties/null_spec.rb | 7 +++ .../schema/properties/numbers_spec.rb | 14 +++++ .../schema/properties/strings_spec.rb | 7 +++ 5 files changed, 73 insertions(+), 13 deletions(-) diff --git a/lib/ruby_llm/schema/dsl/schema_builders.rb b/lib/ruby_llm/schema/dsl/schema_builders.rb index fff4b02..7b143ee 100644 --- a/lib/ruby_llm/schema/dsl/schema_builders.rb +++ b/lib/ruby_llm/schema/dsl/schema_builders.rb @@ -4,8 +4,14 @@ module RubyLLM class Schema module DSL module SchemaBuilders - def string_schema(description: nil, enum: nil, min_length: nil, max_length: nil, pattern: nil, format: nil) - { + NOT_GIVEN = Object.new.freeze + + def string_schema(description: nil, enum: nil, min_length: nil, max_length: nil, pattern: nil, **options) + format = options.delete(:format) + const = options.delete(:const) { NOT_GIVEN } + raise_unknown_options!("string", options) + + add_const({ type: "string", enum: enum, description: description, @@ -13,11 +19,15 @@ def string_schema(description: nil, enum: nil, min_length: nil, max_length: nil, maxLength: max_length, pattern: pattern, format: format - }.compact + }.compact, const) end - def number_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, multiple_of: nil) - { + def number_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, **options) + multiple_of = options.delete(:multiple_of) + const = options.delete(:const) { NOT_GIVEN } + raise_unknown_options!("number", options) + + add_const({ type: "number", description: description, minimum: minimum, @@ -25,11 +35,15 @@ def number_schema(description: nil, minimum: nil, maximum: nil, greater_than: ni exclusiveMinimum: greater_than, exclusiveMaximum: less_than, multipleOf: multiple_of - }.compact + }.compact, const) end - def integer_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, multiple_of: nil) - { + def integer_schema(description: nil, minimum: nil, maximum: nil, greater_than: nil, less_than: nil, **options) + multiple_of = options.delete(:multiple_of) + const = options.delete(:const) { NOT_GIVEN } + raise_unknown_options!("integer", options) + + add_const({ type: "integer", description: description, minimum: minimum, @@ -37,15 +51,15 @@ def integer_schema(description: nil, minimum: nil, maximum: nil, greater_than: n exclusiveMinimum: greater_than, exclusiveMaximum: less_than, multipleOf: multiple_of - }.compact + }.compact, const) end - def boolean_schema(description: nil) - {type: "boolean", description: description}.compact + def boolean_schema(description: nil, const: NOT_GIVEN) + add_const({type: "boolean", description: description}.compact, const) end - def null_schema(description: nil) - {type: "null", description: description}.compact + def null_schema(description: nil, const: NOT_GIVEN) + add_const({type: "null", description: description}.compact, const) end def object_schema(description: nil, of: nil, reference: nil, &block) @@ -113,6 +127,17 @@ def one_of_schema(description: nil, &block) private + def add_const(schema, const) + schema[:const] = const unless const.equal?(NOT_GIVEN) + schema + end + + def raise_unknown_options!(type, options) + return if options.empty? + + raise ArgumentError, "unknown #{type} schema option: #{options.keys.first.inspect}" + end + def determine_array_items(of, &) return collect_schemas_from_block(&).first if block_given? return send("#{of}_schema") if primitive_type?(of) diff --git a/spec/ruby_llm/schema/properties/booleans_spec.rb b/spec/ruby_llm/schema/properties/booleans_spec.rb index 70f7c5b..db5a6dd 100644 --- a/spec/ruby_llm/schema/properties/booleans_spec.rb +++ b/spec/ruby_llm/schema/properties/booleans_spec.rb @@ -11,4 +11,11 @@ properties = schema_class.properties expect(properties[:enabled]).to eq({type: "boolean", description: "Enabled field"}) end + + it "supports boolean const values" do + schema_class.boolean :enabled, const: false + + properties = schema_class.properties + expect(properties[:enabled]).to eq({type: "boolean", const: false}) + end end diff --git a/spec/ruby_llm/schema/properties/null_spec.rb b/spec/ruby_llm/schema/properties/null_spec.rb index cb9929d..e43b241 100644 --- a/spec/ruby_llm/schema/properties/null_spec.rb +++ b/spec/ruby_llm/schema/properties/null_spec.rb @@ -11,4 +11,11 @@ properties = schema_class.properties expect(properties[:placeholder]).to eq({type: "null", description: "Null field"}) end + + it "supports null const values" do + schema_class.null :deleted_at, const: nil + + properties = schema_class.properties + expect(properties[:deleted_at]).to eq({type: "null", const: nil}) + end end diff --git a/spec/ruby_llm/schema/properties/numbers_spec.rb b/spec/ruby_llm/schema/properties/numbers_spec.rb index 8a57bc3..d1decf2 100644 --- a/spec/ruby_llm/schema/properties/numbers_spec.rb +++ b/spec/ruby_llm/schema/properties/numbers_spec.rb @@ -29,6 +29,13 @@ }) end + it "supports number const values" do + schema_class.number :version, const: 1.5 + + properties = schema_class.properties + expect(properties[:version]).to eq({type: "number", const: 1.5}) + end + it "supports number type with description" do schema_class.number :price, description: "Price field" @@ -53,4 +60,11 @@ exclusiveMaximum: 66 }) end + + it "supports integer const values" do + schema_class.integer :version, const: 1 + + properties = schema_class.properties + expect(properties[:version]).to eq({type: "integer", const: 1}) + end end diff --git a/spec/ruby_llm/schema/properties/strings_spec.rb b/spec/ruby_llm/schema/properties/strings_spec.rb index 86a6a92..54d1bc2 100644 --- a/spec/ruby_llm/schema/properties/strings_spec.rb +++ b/spec/ruby_llm/schema/properties/strings_spec.rb @@ -16,6 +16,13 @@ }) end + it "supports string const values" do + schema_class.string :role, const: "admin" + + properties = schema_class.properties + expect(properties[:role]).to eq({type: "string", const: "admin"}) + end + it "supports string type with additional options" do schema_class.string :email, format: "email", min_length: 5, max_length: 100, pattern: "\\S+@\\S+", description: "Email field"