lluminary 0.1.1 → 0.1.3
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/lib/lluminary/config.rb +6 -1
- data/lib/lluminary/models/base.rb +235 -0
- data/lib/lluminary/models/bedrock/amazon_nova_pro_v1.rb +19 -0
- data/lib/lluminary/models/bedrock/anthropic_claude_instant_v1.rb +17 -0
- data/lib/lluminary/models/bedrock/base.rb +29 -0
- data/lib/lluminary/models/openai/gpt35_turbo.rb +20 -0
- data/lib/lluminary/provider_error.rb +2 -1
- data/lib/lluminary/providers/base.rb +20 -3
- data/lib/lluminary/providers/bedrock.rb +52 -32
- data/lib/lluminary/providers/openai.rb +41 -24
- data/lib/lluminary/providers/test.rb +14 -13
- data/lib/lluminary/result.rb +5 -2
- data/lib/lluminary/schema.rb +59 -15
- data/lib/lluminary/schema_model.rb +67 -10
- data/lib/lluminary/task.rb +58 -99
- data/lib/lluminary/validation_error.rb +2 -1
- data/lib/lluminary/version.rb +3 -2
- data/lib/lluminary.rb +25 -7
- data/spec/examples/analyze_text_spec.rb +7 -4
- data/spec/examples/color_analyzer_spec.rb +22 -22
- data/spec/examples/content_analyzer_spec.rb +27 -44
- data/spec/examples/historical_event_analyzer_spec.rb +18 -15
- data/spec/examples/meal_suggester_spec.rb +64 -0
- data/spec/examples/price_analyzer_spec.rb +22 -28
- data/spec/examples/quote_task_spec.rb +9 -8
- data/spec/examples/sentiment_analysis_spec.rb +13 -10
- data/spec/examples/summarize_text_spec.rb +7 -4
- data/spec/lluminary/config_spec.rb +28 -26
- data/spec/lluminary/models/base_spec.rb +581 -0
- data/spec/lluminary/models/bedrock/amazon_nova_pro_v1_spec.rb +30 -0
- data/spec/lluminary/models/bedrock/anthropic_claude_instant_v1_spec.rb +21 -0
- data/spec/lluminary/models/openai/gpt35_turbo_spec.rb +22 -0
- data/spec/lluminary/providers/bedrock_spec.rb +86 -57
- data/spec/lluminary/providers/openai_spec.rb +58 -34
- data/spec/lluminary/providers/test_spec.rb +46 -16
- data/spec/lluminary/result_spec.rb +17 -10
- data/spec/lluminary/schema_model_spec.rb +108 -22
- data/spec/lluminary/schema_spec.rb +241 -107
- data/spec/lluminary/task_spec.rb +118 -584
- data/spec/spec_helper.rb +8 -2
- metadata +73 -22
- data/lib/lluminary/field_description.rb +0 -148
- data/spec/lluminary/field_description_spec.rb +0 -36
- data/spec/lluminary/providers/base_spec.rb +0 -17
@@ -1,109 +1,138 @@
|
|
1
|
-
|
2
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
require "lluminary/providers/bedrock"
|
3
4
|
|
4
5
|
RSpec.describe Lluminary::Providers::Bedrock do
|
5
6
|
let(:config) do
|
6
7
|
{
|
7
|
-
region:
|
8
|
-
access_key_id:
|
9
|
-
secret_access_key:
|
8
|
+
region: "us-east-1",
|
9
|
+
access_key_id: "test-key",
|
10
|
+
secret_access_key: "test-secret"
|
10
11
|
}
|
11
12
|
end
|
12
13
|
let(:provider) { described_class.new(**config) }
|
13
14
|
|
14
|
-
describe
|
15
|
-
it
|
15
|
+
describe "#client" do
|
16
|
+
it "returns the AWS Bedrock client instance" do
|
16
17
|
expect(provider.client).to be_a(Aws::BedrockRuntime::Client)
|
17
18
|
end
|
18
19
|
end
|
19
20
|
|
20
|
-
describe
|
21
|
-
let(:
|
22
|
-
|
21
|
+
describe "#models" do
|
22
|
+
let(:mock_models_response) do
|
23
|
+
OpenStruct.new(
|
24
|
+
foundation_models: [
|
25
|
+
OpenStruct.new(
|
26
|
+
model_id: "anthropic.claude-instant-v1",
|
27
|
+
model_name: "Claude Instant",
|
28
|
+
provider_name: "Anthropic",
|
29
|
+
input_modalities: ["TEXT"],
|
30
|
+
output_modalities: ["TEXT"],
|
31
|
+
customizations_supported: []
|
32
|
+
),
|
33
|
+
OpenStruct.new(
|
34
|
+
model_id: "anthropic.claude-v2",
|
35
|
+
model_name: "Claude V2",
|
36
|
+
provider_name: "Anthropic",
|
37
|
+
input_modalities: ["TEXT"],
|
38
|
+
output_modalities: ["TEXT"],
|
39
|
+
customizations_supported: []
|
40
|
+
)
|
41
|
+
]
|
42
|
+
)
|
43
|
+
end
|
44
|
+
|
45
|
+
before do
|
46
|
+
models_client = double("BedrockClient")
|
47
|
+
allow(Aws::Bedrock::Client).to receive(:new).and_return(models_client)
|
48
|
+
allow(models_client).to receive(:list_foundation_models).and_return(
|
49
|
+
mock_models_response
|
50
|
+
)
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns an array of model IDs as strings" do
|
54
|
+
expect(provider.models).to eq(
|
55
|
+
%w[anthropic.claude-instant-v1 anthropic.claude-v2]
|
56
|
+
)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
describe "#call" do
|
61
|
+
let(:prompt) { "Test prompt" }
|
62
|
+
let(:task) { "Test task" }
|
23
63
|
let(:mock_response) do
|
24
64
|
OpenStruct.new(
|
25
|
-
output:
|
26
|
-
|
27
|
-
|
28
|
-
OpenStruct.new(
|
29
|
-
|
65
|
+
output:
|
66
|
+
OpenStruct.new(
|
67
|
+
message:
|
68
|
+
OpenStruct.new(
|
69
|
+
content: [OpenStruct.new(text: '{"sentiment": "positive"}')]
|
70
|
+
)
|
30
71
|
)
|
31
|
-
)
|
32
72
|
)
|
33
73
|
end
|
34
74
|
|
35
75
|
before do
|
36
|
-
allow_any_instance_of(Aws::BedrockRuntime::Client).to receive(
|
76
|
+
allow_any_instance_of(Aws::BedrockRuntime::Client).to receive(
|
77
|
+
:converse
|
78
|
+
).and_return(mock_response)
|
37
79
|
end
|
38
80
|
|
39
|
-
it
|
81
|
+
it "returns a hash with raw and parsed response" do
|
40
82
|
response = provider.call(prompt, task)
|
41
|
-
expect(response).to eq(
|
42
|
-
|
43
|
-
|
44
|
-
|
83
|
+
expect(response).to eq(
|
84
|
+
{
|
85
|
+
raw: '{"sentiment": "positive"}',
|
86
|
+
parsed: {
|
87
|
+
"sentiment" => "positive"
|
88
|
+
}
|
89
|
+
}
|
90
|
+
)
|
45
91
|
end
|
46
92
|
|
47
|
-
context
|
93
|
+
context "when the response is not valid JSON" do
|
48
94
|
let(:mock_response) do
|
49
95
|
OpenStruct.new(
|
50
|
-
output:
|
51
|
-
|
52
|
-
|
53
|
-
OpenStruct.new(
|
54
|
-
|
96
|
+
output:
|
97
|
+
OpenStruct.new(
|
98
|
+
message:
|
99
|
+
OpenStruct.new(
|
100
|
+
content: [OpenStruct.new(text: "not valid json")]
|
101
|
+
)
|
55
102
|
)
|
56
|
-
)
|
57
103
|
)
|
58
104
|
end
|
59
105
|
|
60
|
-
it
|
106
|
+
it "returns raw response with nil parsed value" do
|
61
107
|
response = provider.call(prompt, task)
|
62
|
-
expect(response).to eq({
|
63
|
-
raw: 'not valid json',
|
64
|
-
parsed: nil
|
65
|
-
})
|
108
|
+
expect(response).to eq({ raw: "not valid json", parsed: nil })
|
66
109
|
end
|
67
110
|
end
|
68
111
|
|
69
|
-
context
|
112
|
+
context "when the response content is nil" do
|
70
113
|
let(:mock_response) do
|
71
114
|
OpenStruct.new(
|
72
|
-
output: OpenStruct.new(
|
73
|
-
message: OpenStruct.new(
|
74
|
-
content: nil
|
75
|
-
)
|
76
|
-
)
|
115
|
+
output: OpenStruct.new(message: OpenStruct.new(content: nil))
|
77
116
|
)
|
78
117
|
end
|
79
118
|
|
80
|
-
it
|
119
|
+
it "returns nil for both raw and parsed values" do
|
81
120
|
response = provider.call(prompt, task)
|
82
|
-
expect(response).to eq({
|
83
|
-
raw: nil,
|
84
|
-
parsed: nil
|
85
|
-
})
|
121
|
+
expect(response).to eq({ raw: nil, parsed: nil })
|
86
122
|
end
|
87
123
|
end
|
88
124
|
|
89
|
-
context
|
125
|
+
context "when the response content array is empty" do
|
90
126
|
let(:mock_response) do
|
91
127
|
OpenStruct.new(
|
92
|
-
output: OpenStruct.new(
|
93
|
-
message: OpenStruct.new(
|
94
|
-
content: []
|
95
|
-
)
|
96
|
-
)
|
128
|
+
output: OpenStruct.new(message: OpenStruct.new(content: []))
|
97
129
|
)
|
98
130
|
end
|
99
131
|
|
100
|
-
it
|
132
|
+
it "returns nil for both raw and parsed values" do
|
101
133
|
response = provider.call(prompt, task)
|
102
|
-
expect(response).to eq({
|
103
|
-
raw: nil,
|
104
|
-
parsed: nil
|
105
|
-
})
|
134
|
+
expect(response).to eq({ raw: nil, parsed: nil })
|
106
135
|
end
|
107
136
|
end
|
108
137
|
end
|
109
|
-
end
|
138
|
+
end
|
@@ -1,62 +1,86 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
2
3
|
|
3
4
|
RSpec.describe Lluminary::Providers::OpenAI do
|
4
|
-
let(:config) { { api_key:
|
5
|
+
let(:config) { { api_key: "test-key" } }
|
5
6
|
let(:provider) { described_class.new(**config) }
|
6
7
|
|
7
|
-
describe
|
8
|
-
it
|
8
|
+
describe "#client" do
|
9
|
+
it "returns the OpenAI client instance" do
|
9
10
|
expect(provider.client).to be_a(OpenAI::Client)
|
10
11
|
end
|
11
12
|
end
|
12
13
|
|
13
|
-
describe
|
14
|
-
let(:
|
15
|
-
let(:task) { 'Test task' }
|
16
|
-
let(:mock_response) do
|
14
|
+
describe "#models" do
|
15
|
+
let(:mock_models_response) do
|
17
16
|
{
|
18
|
-
|
17
|
+
"object" => "list",
|
18
|
+
"data" => [
|
19
|
+
{
|
20
|
+
"id" => "gpt-4",
|
21
|
+
"object" => "model",
|
22
|
+
"created" => 1_687_882_411,
|
23
|
+
"owned_by" => "openai"
|
24
|
+
},
|
19
25
|
{
|
20
|
-
|
21
|
-
|
22
|
-
|
26
|
+
"id" => "gpt-3.5-turbo",
|
27
|
+
"object" => "model",
|
28
|
+
"created" => 1_677_610_602,
|
29
|
+
"owned_by" => "openai"
|
23
30
|
}
|
24
31
|
]
|
25
32
|
}
|
26
33
|
end
|
27
34
|
|
28
35
|
before do
|
29
|
-
allow_any_instance_of(OpenAI::Client).to receive(:
|
36
|
+
allow_any_instance_of(OpenAI::Client).to receive(:models).and_return(
|
37
|
+
double("ModelsClient", list: mock_models_response)
|
38
|
+
)
|
30
39
|
end
|
31
40
|
|
32
|
-
it
|
33
|
-
|
34
|
-
expect(response).to eq({
|
35
|
-
raw: '{"summary": "Test response"}',
|
36
|
-
parsed: { 'summary' => 'Test response' }
|
37
|
-
})
|
41
|
+
it "returns an array of model IDs as strings" do
|
42
|
+
expect(provider.models).to eq(%w[gpt-4 gpt-3.5-turbo])
|
38
43
|
end
|
44
|
+
end
|
39
45
|
|
40
|
-
|
41
|
-
|
46
|
+
describe "#call" do
|
47
|
+
let(:prompt) { "Test prompt" }
|
48
|
+
let(:task) { "Test task" }
|
49
|
+
let(:mock_response) do
|
50
|
+
{
|
51
|
+
"choices" => [
|
52
|
+
{ "message" => { "content" => '{"summary": "Test response"}' } }
|
53
|
+
]
|
54
|
+
}
|
55
|
+
end
|
56
|
+
|
57
|
+
before do
|
58
|
+
allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(
|
59
|
+
mock_response
|
60
|
+
)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "returns a hash with raw and parsed response" do
|
64
|
+
response = provider.call(prompt, task)
|
65
|
+
expect(response).to eq(
|
42
66
|
{
|
43
|
-
'
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
}
|
48
|
-
}
|
49
|
-
]
|
67
|
+
raw: '{"summary": "Test response"}',
|
68
|
+
parsed: {
|
69
|
+
"summary" => "Test response"
|
70
|
+
}
|
50
71
|
}
|
72
|
+
)
|
73
|
+
end
|
74
|
+
|
75
|
+
context "when the response is not valid JSON" do
|
76
|
+
let(:mock_response) do
|
77
|
+
{ "choices" => [{ "message" => { "content" => "not valid json" } }] }
|
51
78
|
end
|
52
79
|
|
53
|
-
it
|
80
|
+
it "returns raw response with nil parsed value" do
|
54
81
|
response = provider.call(prompt, task)
|
55
|
-
expect(response).to eq({
|
56
|
-
raw: 'not valid json',
|
57
|
-
parsed: nil
|
58
|
-
})
|
82
|
+
expect(response).to eq({ raw: "not valid json", parsed: nil })
|
59
83
|
end
|
60
84
|
end
|
61
85
|
end
|
62
|
-
end
|
86
|
+
end
|
@@ -1,14 +1,42 @@
|
|
1
|
-
|
2
|
-
require
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
require "lluminary/providers/test"
|
3
4
|
|
4
5
|
RSpec.describe Lluminary::Providers::Test do
|
5
6
|
let(:provider) { described_class.new }
|
6
7
|
let(:prompt) { "Test prompt" }
|
7
|
-
let(:task_class)
|
8
|
+
let(:task_class) do
|
9
|
+
double("TaskClass", output_fields: { summary: { type: :string } })
|
10
|
+
end
|
8
11
|
let(:task) { double("Task", class: task_class) }
|
9
12
|
|
10
|
-
describe
|
11
|
-
it
|
13
|
+
describe "#initialize" do
|
14
|
+
it "accepts configuration options" do
|
15
|
+
config = { api_key: "test_key", model: "test_model" }
|
16
|
+
provider = described_class.new(**config)
|
17
|
+
expect(provider.config).to eq(config)
|
18
|
+
end
|
19
|
+
|
20
|
+
it "merges default provider configuration with instance configuration" do
|
21
|
+
# Set up default provider configuration
|
22
|
+
Lluminary.configure do |config|
|
23
|
+
config.provider(:test, api_key: "global_key", model: "global_model")
|
24
|
+
end
|
25
|
+
|
26
|
+
# Create provider with instance-specific config
|
27
|
+
provider = described_class.new(model: "instance_model", temperature: 0.7)
|
28
|
+
|
29
|
+
# Should merge default and instance config, with instance taking precedence
|
30
|
+
expect(provider.config).to eq(
|
31
|
+
api_key: "global_key",
|
32
|
+
model: "instance_model",
|
33
|
+
temperature: 0.7
|
34
|
+
)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
describe "#call" do
|
39
|
+
it "returns a hash with raw and parsed response" do
|
12
40
|
response = provider.call(prompt, task)
|
13
41
|
|
14
42
|
expect(response).to be_a(Hash)
|
@@ -16,7 +44,7 @@ RSpec.describe Lluminary::Providers::Test do
|
|
16
44
|
expect(response[:parsed]).to eq({ "summary" => "Test string value" })
|
17
45
|
end
|
18
46
|
|
19
|
-
it
|
47
|
+
it "handles prompts with schema descriptions" do
|
20
48
|
prompt_with_schema = <<~PROMPT
|
21
49
|
Test prompt
|
22
50
|
|
@@ -36,22 +64,24 @@ RSpec.describe Lluminary::Providers::Test do
|
|
36
64
|
expect(response[:parsed]).to eq({ "summary" => "Test string value" })
|
37
65
|
end
|
38
66
|
|
39
|
-
it
|
40
|
-
task_class =
|
67
|
+
it "generates integer values for integer fields" do
|
68
|
+
task_class =
|
69
|
+
double("TaskClass", output_fields: { count: { type: :integer } })
|
41
70
|
task = double("Task", class: task_class)
|
42
|
-
|
71
|
+
|
43
72
|
response = provider.call(prompt, task)
|
44
73
|
expect(response[:raw]).to eq('{"count": 0}')
|
45
74
|
expect(response[:parsed]).to eq({ "count" => 0 })
|
46
75
|
end
|
47
76
|
|
48
|
-
it
|
49
|
-
task_class =
|
77
|
+
it "raises error for unsupported types" do
|
78
|
+
task_class =
|
79
|
+
double("TaskClass", output_fields: { value: { type: :unsupported } })
|
50
80
|
task = double("Task", class: task_class)
|
51
|
-
|
52
|
-
expect {
|
53
|
-
|
54
|
-
|
81
|
+
|
82
|
+
expect { provider.call(prompt, task) }.to raise_error(
|
83
|
+
"Unsupported type: unsupported"
|
84
|
+
)
|
55
85
|
end
|
56
86
|
end
|
57
|
-
end
|
87
|
+
end
|
@@ -1,31 +1,38 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "lluminary"
|
2
3
|
|
3
4
|
RSpec.describe Lluminary::Result do
|
4
5
|
let(:raw_response) { "Test response" }
|
5
6
|
let(:output) { { summary: "Test summary" } }
|
6
7
|
let(:prompt) { "Test prompt" }
|
7
|
-
let(:result)
|
8
|
+
let(:result) do
|
9
|
+
described_class.new(
|
10
|
+
raw_response: raw_response,
|
11
|
+
output: output,
|
12
|
+
prompt: prompt
|
13
|
+
)
|
14
|
+
end
|
8
15
|
|
9
|
-
describe
|
10
|
-
it
|
16
|
+
describe "#raw_response" do
|
17
|
+
it "returns the raw response" do
|
11
18
|
expect(result.raw_response).to eq(raw_response)
|
12
19
|
end
|
13
20
|
end
|
14
21
|
|
15
|
-
describe
|
16
|
-
it
|
22
|
+
describe "#output" do
|
23
|
+
it "returns an OpenStruct with the output data" do
|
17
24
|
expect(result.output).to be_a(OpenStruct)
|
18
25
|
expect(result.output.summary).to eq(output[:summary])
|
19
26
|
end
|
20
27
|
|
21
|
-
it
|
28
|
+
it "allows accessing output fields as methods" do
|
22
29
|
expect(result.output.summary).to eq(output[:summary])
|
23
30
|
end
|
24
31
|
end
|
25
32
|
|
26
|
-
describe
|
27
|
-
it
|
33
|
+
describe "#prompt" do
|
34
|
+
it "returns the prompt" do
|
28
35
|
expect(result.prompt).to eq(prompt)
|
29
36
|
end
|
30
37
|
end
|
31
|
-
end
|
38
|
+
end
|
@@ -1,11 +1,18 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
2
3
|
|
3
4
|
RSpec.describe Lluminary::SchemaModel do
|
4
|
-
describe
|
5
|
+
describe ".build" do
|
5
6
|
let(:fields) do
|
6
7
|
{
|
7
|
-
name: {
|
8
|
-
|
8
|
+
name: {
|
9
|
+
type: :string,
|
10
|
+
description: "The user's name"
|
11
|
+
},
|
12
|
+
age: {
|
13
|
+
type: :integer,
|
14
|
+
description: "The user's age"
|
15
|
+
}
|
9
16
|
}
|
10
17
|
end
|
11
18
|
|
@@ -16,35 +23,39 @@ RSpec.describe Lluminary::SchemaModel do
|
|
16
23
|
]
|
17
24
|
end
|
18
25
|
|
19
|
-
let(:model_class)
|
26
|
+
let(:model_class) do
|
27
|
+
described_class.build(fields: fields, validations: validations)
|
28
|
+
end
|
20
29
|
|
21
|
-
it
|
30
|
+
it "creates a class that inherits from SchemaModel" do
|
22
31
|
expect(model_class.ancestors).to include(described_class)
|
23
32
|
end
|
24
33
|
|
25
|
-
it
|
34
|
+
it "includes ActiveModel::Validations" do
|
26
35
|
expect(model_class.ancestors).to include(ActiveModel::Validations)
|
27
36
|
end
|
28
37
|
|
29
|
-
it
|
38
|
+
it "adds accessors for fields" do
|
30
39
|
instance = model_class.new(name: "John", age: 30)
|
31
40
|
expect(instance.name).to eq("John")
|
32
41
|
expect(instance.age).to eq(30)
|
33
42
|
end
|
34
43
|
|
35
|
-
it
|
44
|
+
it "validates presence" do
|
36
45
|
instance = model_class.new(age: 30)
|
37
46
|
expect(instance.valid?).to be false
|
38
47
|
expect(instance.errors.full_messages).to include("Name can't be blank")
|
39
48
|
end
|
40
49
|
|
41
|
-
it
|
50
|
+
it "validates numericality" do
|
42
51
|
instance = model_class.new(name: "John", age: 0)
|
43
52
|
expect(instance.valid?).to be false
|
44
|
-
expect(instance.errors.full_messages).to include(
|
53
|
+
expect(instance.errors.full_messages).to include(
|
54
|
+
"Age must be greater than 0"
|
55
|
+
)
|
45
56
|
end
|
46
57
|
|
47
|
-
it
|
58
|
+
it "validates types" do
|
48
59
|
instance = model_class.new(name: 123, age: "30")
|
49
60
|
expect(instance.valid?).to be false
|
50
61
|
expect(instance.errors.full_messages).to include(
|
@@ -53,34 +64,109 @@ RSpec.describe Lluminary::SchemaModel do
|
|
53
64
|
)
|
54
65
|
end
|
55
66
|
|
56
|
-
it
|
57
|
-
fields = {
|
58
|
-
price: { type: :float, description: "The price" }
|
59
|
-
}
|
67
|
+
it "validates float types" do
|
68
|
+
fields = { price: { type: :float, description: "The price" } }
|
60
69
|
model_class = described_class.build(fields: fields, validations: [])
|
61
|
-
|
70
|
+
|
62
71
|
# Test that nil is allowed
|
63
72
|
instance = model_class.new(price: nil)
|
64
73
|
expect(instance.valid?).to be true
|
65
|
-
|
74
|
+
|
66
75
|
# Test invalid float value
|
67
76
|
instance = model_class.new(price: "not a float")
|
68
77
|
expect(instance.valid?).to be false
|
69
78
|
expect(instance.errors.full_messages).to include("Price must be a float")
|
70
|
-
|
79
|
+
|
71
80
|
# Test valid float value
|
72
81
|
instance = model_class.new(price: 12.34)
|
73
82
|
expect(instance.valid?).to be true
|
74
83
|
end
|
75
84
|
|
76
|
-
it
|
85
|
+
it "accepts valid attributes" do
|
77
86
|
instance = model_class.new(name: "John", age: 30)
|
78
87
|
expect(instance.valid?).to be true
|
79
88
|
end
|
80
89
|
|
81
|
-
it
|
90
|
+
it "provides access to raw attributes" do
|
82
91
|
instance = model_class.new(name: "John", age: 30)
|
83
92
|
expect(instance.attributes).to eq({ "name" => "John", "age" => 30 })
|
84
93
|
end
|
94
|
+
|
95
|
+
it "validates array types" do
|
96
|
+
fields = {
|
97
|
+
items: {
|
98
|
+
type: :array,
|
99
|
+
element_type: {
|
100
|
+
type: :string,
|
101
|
+
description: nil
|
102
|
+
},
|
103
|
+
description: "A list of strings"
|
104
|
+
}
|
105
|
+
}
|
106
|
+
model_class = described_class.build(fields: fields, validations: [])
|
107
|
+
|
108
|
+
# Test that nil is allowed
|
109
|
+
instance = model_class.new(items: nil)
|
110
|
+
expect(instance.valid?).to be true
|
111
|
+
|
112
|
+
# Test valid array of strings
|
113
|
+
instance = model_class.new(items: %w[one two])
|
114
|
+
expect(instance.valid?).to be true
|
115
|
+
|
116
|
+
# Test invalid array (not an array)
|
117
|
+
instance = model_class.new(items: "not an array")
|
118
|
+
expect(instance.valid?).to be false
|
119
|
+
expect(instance.errors.full_messages).to include("Items must be an Array")
|
120
|
+
|
121
|
+
# Test invalid array elements
|
122
|
+
instance = model_class.new(items: ["one", 2, "three"])
|
123
|
+
expect(instance.valid?).to be false
|
124
|
+
expect(instance.errors.full_messages).to include(
|
125
|
+
"Items[1] must be a String"
|
126
|
+
)
|
127
|
+
end
|
128
|
+
|
129
|
+
it "validates nested array types" do
|
130
|
+
fields = {
|
131
|
+
matrix: {
|
132
|
+
type: :array,
|
133
|
+
element_type: {
|
134
|
+
type: :array,
|
135
|
+
element_type: {
|
136
|
+
type: :integer,
|
137
|
+
description: nil
|
138
|
+
},
|
139
|
+
description: nil
|
140
|
+
},
|
141
|
+
description: "A matrix of integers"
|
142
|
+
}
|
143
|
+
}
|
144
|
+
model_class = described_class.build(fields: fields, validations: [])
|
145
|
+
|
146
|
+
# Test valid nested arrays
|
147
|
+
instance = model_class.new(matrix: [[1, 2], [3, 4]])
|
148
|
+
expect(instance.valid?).to be true
|
149
|
+
|
150
|
+
# Test invalid outer array
|
151
|
+
instance = model_class.new(matrix: "not an array")
|
152
|
+
expect(instance.valid?).to be false
|
153
|
+
expect(instance.errors.full_messages).to include(
|
154
|
+
"Matrix must be an Array"
|
155
|
+
)
|
156
|
+
|
157
|
+
# Test invalid inner array
|
158
|
+
instance = model_class.new(matrix: ["not an array"])
|
159
|
+
expect(instance.valid?).to be false
|
160
|
+
expect(instance.errors.full_messages).to include(
|
161
|
+
"Matrix[0] must be an Array"
|
162
|
+
)
|
163
|
+
|
164
|
+
# Test invalid inner array elements
|
165
|
+
instance = model_class.new(matrix: [[1, "2"], [3, 4]])
|
166
|
+
expect(instance.valid?).to be false
|
167
|
+
expect(instance.errors.full_messages).to include(
|
168
|
+
"Matrix[0][1] must be an Integer"
|
169
|
+
)
|
170
|
+
end
|
85
171
|
end
|
86
|
-
end
|
172
|
+
end
|