From 314a7f56119f2f9a3cd2d70e7452e48968e73e3f Mon Sep 17 00:00:00 2001 From: Stephen Mariano Cabrera Date: Mon, 10 Nov 2025 08:36:49 -0800 Subject: [PATCH] Add support for a oneOf type --- lib/ruby_llm/schema.rb | 2 +- lib/ruby_llm/schema/dsl/complex_types.rb | 4 ++ lib/ruby_llm/schema/dsl/schema_builders.rb | 9 ++++ spec/ruby_llm/schema_spec.rb | 51 +++++++++++++++++++++- 4 files changed, 64 insertions(+), 2 deletions(-) diff --git a/lib/ruby_llm/schema.rb b/lib/ruby_llm/schema.rb index 1a380ce..eab18a7 100644 --- a/lib/ruby_llm/schema.rb +++ b/lib/ruby_llm/schema.rb @@ -84,7 +84,7 @@ def method_missing(method_name, ...) end def respond_to_missing?(method_name, include_private = false) - %i[string number integer boolean array object any_of null].include?(method_name) || super + %i[string number integer boolean array object any_of one_of null].include?(method_name) || super end end end diff --git a/lib/ruby_llm/schema/dsl/complex_types.rb b/lib/ruby_llm/schema/dsl/complex_types.rb index d109cb7..fe5be49 100644 --- a/lib/ruby_llm/schema/dsl/complex_types.rb +++ b/lib/ruby_llm/schema/dsl/complex_types.rb @@ -16,6 +16,10 @@ def any_of(name, description: nil, required: true, **options, &block) add_property(name, any_of_schema(description: description, **options, &block), required: required) end + def one_of(name, description: nil, required: true, **options, &block) + add_property(name, one_of_schema(description: description, **options, &block), required: required) + end + def optional(name, description: nil, &block) any_of(name, description: description) do instance_eval(&block) diff --git a/lib/ruby_llm/schema/dsl/schema_builders.rb b/lib/ruby_llm/schema/dsl/schema_builders.rb index 8ce3993..91ca52b 100644 --- a/lib/ruby_llm/schema/dsl/schema_builders.rb +++ b/lib/ruby_llm/schema/dsl/schema_builders.rb @@ -96,6 +96,15 @@ def any_of_schema(description: nil, &block) }.compact end + def one_of_schema(description: nil, &block) + schemas = collect_schemas_from_block(&block) + + { + description: description, + oneOf: schemas + }.compact + end + private def determine_array_items(of, &) diff --git a/spec/ruby_llm/schema_spec.rb b/spec/ruby_llm/schema_spec.rb index e75e520..178864b 100644 --- a/spec/ruby_llm/schema_spec.rb +++ b/spec/ruby_llm/schema_spec.rb @@ -154,6 +154,32 @@ end end end + + it "supports arrays of oneOf types" do + schema_class.array :items do + one_of :value do + string :alphanumeric + number :numeric + end + end + end + + it "supports basic oneOf with primitive types" do + schema_class.one_of :status do + string enum: %w[active inactive] + integer + boolean + end + + properties = schema_class.properties + one_of_schemas = properties[:status][:oneOf] + + expect(one_of_schemas).to include( + {type: "string", enum: %w[active inactive]}, + {type: "integer"}, + {type: "boolean"} + ) + end end # =========================================== @@ -221,6 +247,29 @@ expect(object_schema[:properties][:nested_field]).to eq({type: "string"}) end + it "supports one_of with mixed types including objects" do + schema_class.one_of :exclusive_field do + string enum: %w[option1 option2] + integer + object do + string :nested_field + end + null + end + + properties = schema_class.properties + one_of_schemas = properties[:exclusive_field][:oneOf] + + expect(one_of_schemas).to include( + {type: "string", enum: %w[option1 option2]}, + {type: "integer"}, + {type: "null"} + ) + + object_schema = one_of_schemas.find { |s| s[:type] == "object" } + expect(object_schema[:properties][:nested_field]).to eq({type: "string"}) + end + it "supports reference to a defined schema by block" do schema_class.define :address do string :street @@ -368,7 +417,7 @@ it "supports method delegation for schema methods" do instance = schema_class.new - expect(instance).to respond_to(:string, :number, :integer, :boolean, :array, :object, :any_of, :null) + expect(instance).to respond_to(:string, :number, :integer, :boolean, :array, :object, :any_of, :one_of, :null) expect(instance).not_to respond_to(:unknown_method) end