lluminary 0.1.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 +7 -0
- data/lib/lluminary/config.rb +23 -0
- data/lib/lluminary/field_description.rb +148 -0
- data/lib/lluminary/provider_error.rb +4 -0
- data/lib/lluminary/providers/base.rb +15 -0
- data/lib/lluminary/providers/bedrock.rb +51 -0
- data/lib/lluminary/providers/openai.rb +40 -0
- data/lib/lluminary/providers/test.rb +37 -0
- data/lib/lluminary/result.rb +13 -0
- data/lib/lluminary/schema.rb +61 -0
- data/lib/lluminary/schema_model.rb +87 -0
- data/lib/lluminary/task.rb +224 -0
- data/lib/lluminary/validation_error.rb +4 -0
- data/lib/lluminary/version.rb +3 -0
- data/lib/lluminary.rb +18 -0
- data/spec/examples/analyze_text_spec.rb +21 -0
- data/spec/examples/color_analyzer_spec.rb +42 -0
- data/spec/examples/content_analyzer_spec.rb +75 -0
- data/spec/examples/historical_event_analyzer_spec.rb +37 -0
- data/spec/examples/price_analyzer_spec.rb +46 -0
- data/spec/examples/quote_task_spec.rb +27 -0
- data/spec/examples/sentiment_analysis_spec.rb +45 -0
- data/spec/examples/summarize_text_spec.rb +21 -0
- data/spec/lluminary/config_spec.rb +53 -0
- data/spec/lluminary/field_description_spec.rb +36 -0
- data/spec/lluminary/providers/base_spec.rb +17 -0
- data/spec/lluminary/providers/bedrock_spec.rb +109 -0
- data/spec/lluminary/providers/openai_spec.rb +62 -0
- data/spec/lluminary/providers/test_spec.rb +57 -0
- data/spec/lluminary/result_spec.rb +31 -0
- data/spec/lluminary/schema_model_spec.rb +86 -0
- data/spec/lluminary/schema_spec.rb +302 -0
- data/spec/lluminary/task_spec.rb +777 -0
- data/spec/spec_helper.rb +4 -0
- metadata +190 -0
@@ -0,0 +1,109 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lluminary/providers/bedrock'
|
3
|
+
|
4
|
+
RSpec.describe Lluminary::Providers::Bedrock do
|
5
|
+
let(:config) do
|
6
|
+
{
|
7
|
+
region: 'us-east-1',
|
8
|
+
access_key_id: 'test-key',
|
9
|
+
secret_access_key: 'test-secret'
|
10
|
+
}
|
11
|
+
end
|
12
|
+
let(:provider) { described_class.new(**config) }
|
13
|
+
|
14
|
+
describe '#client' do
|
15
|
+
it 'returns the AWS Bedrock client instance' do
|
16
|
+
expect(provider.client).to be_a(Aws::BedrockRuntime::Client)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
describe '#call' do
|
21
|
+
let(:prompt) { 'Test prompt' }
|
22
|
+
let(:task) { 'Test task' }
|
23
|
+
let(:mock_response) do
|
24
|
+
OpenStruct.new(
|
25
|
+
output: OpenStruct.new(
|
26
|
+
message: OpenStruct.new(
|
27
|
+
content: [
|
28
|
+
OpenStruct.new(text: '{"sentiment": "positive"}')
|
29
|
+
]
|
30
|
+
)
|
31
|
+
)
|
32
|
+
)
|
33
|
+
end
|
34
|
+
|
35
|
+
before do
|
36
|
+
allow_any_instance_of(Aws::BedrockRuntime::Client).to receive(:converse).and_return(mock_response)
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'returns a hash with raw and parsed response' do
|
40
|
+
response = provider.call(prompt, task)
|
41
|
+
expect(response).to eq({
|
42
|
+
raw: '{"sentiment": "positive"}',
|
43
|
+
parsed: { 'sentiment' => 'positive' }
|
44
|
+
})
|
45
|
+
end
|
46
|
+
|
47
|
+
context 'when the response is not valid JSON' do
|
48
|
+
let(:mock_response) do
|
49
|
+
OpenStruct.new(
|
50
|
+
output: OpenStruct.new(
|
51
|
+
message: OpenStruct.new(
|
52
|
+
content: [
|
53
|
+
OpenStruct.new(text: 'not valid json')
|
54
|
+
]
|
55
|
+
)
|
56
|
+
)
|
57
|
+
)
|
58
|
+
end
|
59
|
+
|
60
|
+
it 'returns raw response with nil parsed value' do
|
61
|
+
response = provider.call(prompt, task)
|
62
|
+
expect(response).to eq({
|
63
|
+
raw: 'not valid json',
|
64
|
+
parsed: nil
|
65
|
+
})
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
context 'when the response content is nil' do
|
70
|
+
let(:mock_response) do
|
71
|
+
OpenStruct.new(
|
72
|
+
output: OpenStruct.new(
|
73
|
+
message: OpenStruct.new(
|
74
|
+
content: nil
|
75
|
+
)
|
76
|
+
)
|
77
|
+
)
|
78
|
+
end
|
79
|
+
|
80
|
+
it 'returns nil for both raw and parsed values' do
|
81
|
+
response = provider.call(prompt, task)
|
82
|
+
expect(response).to eq({
|
83
|
+
raw: nil,
|
84
|
+
parsed: nil
|
85
|
+
})
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
context 'when the response content array is empty' do
|
90
|
+
let(:mock_response) do
|
91
|
+
OpenStruct.new(
|
92
|
+
output: OpenStruct.new(
|
93
|
+
message: OpenStruct.new(
|
94
|
+
content: []
|
95
|
+
)
|
96
|
+
)
|
97
|
+
)
|
98
|
+
end
|
99
|
+
|
100
|
+
it 'returns nil for both raw and parsed values' do
|
101
|
+
response = provider.call(prompt, task)
|
102
|
+
expect(response).to eq({
|
103
|
+
raw: nil,
|
104
|
+
parsed: nil
|
105
|
+
})
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
109
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lluminary::Providers::OpenAI do
|
4
|
+
let(:config) { { api_key: 'test-key' } }
|
5
|
+
let(:provider) { described_class.new(**config) }
|
6
|
+
|
7
|
+
describe '#client' do
|
8
|
+
it 'returns the OpenAI client instance' do
|
9
|
+
expect(provider.client).to be_a(OpenAI::Client)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe '#call' do
|
14
|
+
let(:prompt) { 'Test prompt' }
|
15
|
+
let(:task) { 'Test task' }
|
16
|
+
let(:mock_response) do
|
17
|
+
{
|
18
|
+
'choices' => [
|
19
|
+
{
|
20
|
+
'message' => {
|
21
|
+
'content' => '{"summary": "Test response"}'
|
22
|
+
}
|
23
|
+
}
|
24
|
+
]
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
before do
|
29
|
+
allow_any_instance_of(OpenAI::Client).to receive(:chat).and_return(mock_response)
|
30
|
+
end
|
31
|
+
|
32
|
+
it 'returns a hash with raw and parsed response' do
|
33
|
+
response = provider.call(prompt, task)
|
34
|
+
expect(response).to eq({
|
35
|
+
raw: '{"summary": "Test response"}',
|
36
|
+
parsed: { 'summary' => 'Test response' }
|
37
|
+
})
|
38
|
+
end
|
39
|
+
|
40
|
+
context 'when the response is not valid JSON' do
|
41
|
+
let(:mock_response) do
|
42
|
+
{
|
43
|
+
'choices' => [
|
44
|
+
{
|
45
|
+
'message' => {
|
46
|
+
'content' => 'not valid json'
|
47
|
+
}
|
48
|
+
}
|
49
|
+
]
|
50
|
+
}
|
51
|
+
end
|
52
|
+
|
53
|
+
it 'returns raw response with nil parsed value' do
|
54
|
+
response = provider.call(prompt, task)
|
55
|
+
expect(response).to eq({
|
56
|
+
raw: 'not valid json',
|
57
|
+
parsed: nil
|
58
|
+
})
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
require 'lluminary/providers/test'
|
3
|
+
|
4
|
+
RSpec.describe Lluminary::Providers::Test do
|
5
|
+
let(:provider) { described_class.new }
|
6
|
+
let(:prompt) { "Test prompt" }
|
7
|
+
let(:task_class) { double("TaskClass", output_fields: { summary: { type: :string } }) }
|
8
|
+
let(:task) { double("Task", class: task_class) }
|
9
|
+
|
10
|
+
describe '#call' do
|
11
|
+
it 'returns a hash with raw and parsed response' do
|
12
|
+
response = provider.call(prompt, task)
|
13
|
+
|
14
|
+
expect(response).to be_a(Hash)
|
15
|
+
expect(response[:raw]).to eq('{"summary": "Test string value"}')
|
16
|
+
expect(response[:parsed]).to eq({ "summary" => "Test string value" })
|
17
|
+
end
|
18
|
+
|
19
|
+
it 'handles prompts with schema descriptions' do
|
20
|
+
prompt_with_schema = <<~PROMPT
|
21
|
+
Test prompt
|
22
|
+
|
23
|
+
You must respond with a valid JSON object with the following fields:
|
24
|
+
|
25
|
+
summary (string): A brief summary of the message
|
26
|
+
Example: "your summary here"
|
27
|
+
|
28
|
+
Your response should look like this:
|
29
|
+
{
|
30
|
+
"summary": "your summary here"
|
31
|
+
}
|
32
|
+
PROMPT
|
33
|
+
|
34
|
+
response = provider.call(prompt_with_schema, task)
|
35
|
+
expect(response[:raw]).to eq('{"summary": "Test string value"}')
|
36
|
+
expect(response[:parsed]).to eq({ "summary" => "Test string value" })
|
37
|
+
end
|
38
|
+
|
39
|
+
it 'generates integer values for integer fields' do
|
40
|
+
task_class = double("TaskClass", output_fields: { count: { type: :integer } })
|
41
|
+
task = double("Task", class: task_class)
|
42
|
+
|
43
|
+
response = provider.call(prompt, task)
|
44
|
+
expect(response[:raw]).to eq('{"count": 0}')
|
45
|
+
expect(response[:parsed]).to eq({ "count" => 0 })
|
46
|
+
end
|
47
|
+
|
48
|
+
it 'raises error for unsupported types' do
|
49
|
+
task_class = double("TaskClass", output_fields: { value: { type: :unsupported } })
|
50
|
+
task = double("Task", class: task_class)
|
51
|
+
|
52
|
+
expect {
|
53
|
+
provider.call(prompt, task)
|
54
|
+
}.to raise_error("Unsupported type: unsupported")
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'lluminary'
|
2
|
+
|
3
|
+
RSpec.describe Lluminary::Result do
|
4
|
+
let(:raw_response) { "Test response" }
|
5
|
+
let(:output) { { summary: "Test summary" } }
|
6
|
+
let(:prompt) { "Test prompt" }
|
7
|
+
let(:result) { described_class.new(raw_response: raw_response, output: output, prompt: prompt) }
|
8
|
+
|
9
|
+
describe '#raw_response' do
|
10
|
+
it 'returns the raw response' do
|
11
|
+
expect(result.raw_response).to eq(raw_response)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
describe '#output' do
|
16
|
+
it 'returns an OpenStruct with the output data' do
|
17
|
+
expect(result.output).to be_a(OpenStruct)
|
18
|
+
expect(result.output.summary).to eq(output[:summary])
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'allows accessing output fields as methods' do
|
22
|
+
expect(result.output.summary).to eq(output[:summary])
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
describe '#prompt' do
|
27
|
+
it 'returns the prompt' do
|
28
|
+
expect(result.prompt).to eq(prompt)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,86 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lluminary::SchemaModel do
|
4
|
+
describe '.build' do
|
5
|
+
let(:fields) do
|
6
|
+
{
|
7
|
+
name: { type: :string, description: "The user's name" },
|
8
|
+
age: { type: :integer, description: "The user's age" }
|
9
|
+
}
|
10
|
+
end
|
11
|
+
|
12
|
+
let(:validations) do
|
13
|
+
[
|
14
|
+
[[:name], { presence: true }],
|
15
|
+
[[:age], { numericality: { greater_than: 0 } }]
|
16
|
+
]
|
17
|
+
end
|
18
|
+
|
19
|
+
let(:model_class) { described_class.build(fields: fields, validations: validations) }
|
20
|
+
|
21
|
+
it 'creates a class that inherits from SchemaModel' do
|
22
|
+
expect(model_class.ancestors).to include(described_class)
|
23
|
+
end
|
24
|
+
|
25
|
+
it 'includes ActiveModel::Validations' do
|
26
|
+
expect(model_class.ancestors).to include(ActiveModel::Validations)
|
27
|
+
end
|
28
|
+
|
29
|
+
it 'adds accessors for fields' do
|
30
|
+
instance = model_class.new(name: "John", age: 30)
|
31
|
+
expect(instance.name).to eq("John")
|
32
|
+
expect(instance.age).to eq(30)
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'validates presence' do
|
36
|
+
instance = model_class.new(age: 30)
|
37
|
+
expect(instance.valid?).to be false
|
38
|
+
expect(instance.errors.full_messages).to include("Name can't be blank")
|
39
|
+
end
|
40
|
+
|
41
|
+
it 'validates numericality' do
|
42
|
+
instance = model_class.new(name: "John", age: 0)
|
43
|
+
expect(instance.valid?).to be false
|
44
|
+
expect(instance.errors.full_messages).to include("Age must be greater than 0")
|
45
|
+
end
|
46
|
+
|
47
|
+
it 'validates types' do
|
48
|
+
instance = model_class.new(name: 123, age: "30")
|
49
|
+
expect(instance.valid?).to be false
|
50
|
+
expect(instance.errors.full_messages).to include(
|
51
|
+
"Name must be a String",
|
52
|
+
"Age must be an Integer"
|
53
|
+
)
|
54
|
+
end
|
55
|
+
|
56
|
+
it 'validates float types' do
|
57
|
+
fields = {
|
58
|
+
price: { type: :float, description: "The price" }
|
59
|
+
}
|
60
|
+
model_class = described_class.build(fields: fields, validations: [])
|
61
|
+
|
62
|
+
# Test that nil is allowed
|
63
|
+
instance = model_class.new(price: nil)
|
64
|
+
expect(instance.valid?).to be true
|
65
|
+
|
66
|
+
# Test invalid float value
|
67
|
+
instance = model_class.new(price: "not a float")
|
68
|
+
expect(instance.valid?).to be false
|
69
|
+
expect(instance.errors.full_messages).to include("Price must be a float")
|
70
|
+
|
71
|
+
# Test valid float value
|
72
|
+
instance = model_class.new(price: 12.34)
|
73
|
+
expect(instance.valid?).to be true
|
74
|
+
end
|
75
|
+
|
76
|
+
it 'accepts valid attributes' do
|
77
|
+
instance = model_class.new(name: "John", age: 30)
|
78
|
+
expect(instance.valid?).to be true
|
79
|
+
end
|
80
|
+
|
81
|
+
it 'provides access to raw attributes' do
|
82
|
+
instance = model_class.new(name: "John", age: 30)
|
83
|
+
expect(instance.attributes).to eq({ "name" => "John", "age" => 30 })
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
@@ -0,0 +1,302 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
RSpec.describe Lluminary::Schema do
|
4
|
+
let(:schema) { described_class.new }
|
5
|
+
|
6
|
+
describe '#initialize' do
|
7
|
+
it 'creates an empty fields hash' do
|
8
|
+
expect(schema.fields).to eq({})
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
describe '#string' do
|
13
|
+
it 'adds a string field to the schema' do
|
14
|
+
schema.string(:name)
|
15
|
+
expect(schema.fields).to eq({ name: { type: :string, description: nil } })
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'adds a string field with description' do
|
19
|
+
schema.string(:name, description: "The user's full name")
|
20
|
+
expect(schema.fields).to eq({
|
21
|
+
name: {
|
22
|
+
type: :string,
|
23
|
+
description: "The user's full name"
|
24
|
+
}
|
25
|
+
})
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
describe '#integer' do
|
30
|
+
it 'adds an integer field to the schema' do
|
31
|
+
schema.integer(:count)
|
32
|
+
expect(schema.fields).to eq({ count: { type: :integer, description: nil } })
|
33
|
+
end
|
34
|
+
|
35
|
+
it 'adds an integer field with description' do
|
36
|
+
schema.integer(:count, description: "The total number of items")
|
37
|
+
expect(schema.fields).to eq({
|
38
|
+
count: {
|
39
|
+
type: :integer,
|
40
|
+
description: "The total number of items"
|
41
|
+
}
|
42
|
+
})
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
describe '#boolean' do
|
47
|
+
it 'adds a boolean field to the schema' do
|
48
|
+
schema.boolean(:active)
|
49
|
+
expect(schema.fields).to eq({ active: { type: :boolean, description: nil } })
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'adds a boolean field with description' do
|
53
|
+
schema.boolean(:active, description: "Whether the item is active")
|
54
|
+
expect(schema.fields).to eq({
|
55
|
+
active: {
|
56
|
+
type: :boolean,
|
57
|
+
description: "Whether the item is active"
|
58
|
+
}
|
59
|
+
})
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
describe '#float' do
|
64
|
+
it 'adds a float field to the schema' do
|
65
|
+
schema.float(:price)
|
66
|
+
expect(schema.fields).to eq({ price: { type: :float, description: nil } })
|
67
|
+
end
|
68
|
+
|
69
|
+
it 'adds a float field with description' do
|
70
|
+
schema.float(:price, description: "The price of the item")
|
71
|
+
expect(schema.fields).to eq({
|
72
|
+
price: {
|
73
|
+
type: :float,
|
74
|
+
description: "The price of the item"
|
75
|
+
}
|
76
|
+
})
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#datetime' do
|
81
|
+
it 'adds a datetime field to the schema' do
|
82
|
+
schema.datetime(:start_time)
|
83
|
+
expect(schema.fields).to eq({ start_time: { type: :datetime, description: nil } })
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'adds a datetime field with description' do
|
87
|
+
schema.datetime(:start_time, description: "When the event starts")
|
88
|
+
expect(schema.fields).to eq({
|
89
|
+
start_time: {
|
90
|
+
type: :datetime,
|
91
|
+
description: "When the event starts"
|
92
|
+
}
|
93
|
+
})
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe '#fields' do
|
98
|
+
it 'returns the fields hash' do
|
99
|
+
schema.string(:name)
|
100
|
+
expect(schema.fields).to eq({ name: { type: :string, description: nil } })
|
101
|
+
end
|
102
|
+
|
103
|
+
it 'returns the same hash instance' do
|
104
|
+
schema.string(:name)
|
105
|
+
first_call = schema.fields
|
106
|
+
second_call = schema.fields
|
107
|
+
expect(first_call).to be(second_call)
|
108
|
+
end
|
109
|
+
|
110
|
+
context 'with datetime fields' do
|
111
|
+
let(:schema) do
|
112
|
+
described_class.new.tap do |s|
|
113
|
+
s.datetime(:start_time)
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
it 'accepts DateTime values' do
|
118
|
+
errors = schema.validate(start_time: DateTime.now)
|
119
|
+
expect(errors).to be_empty
|
120
|
+
end
|
121
|
+
|
122
|
+
it 'accepts nil values' do
|
123
|
+
errors = schema.validate(start_time: nil)
|
124
|
+
expect(errors).to be_empty
|
125
|
+
end
|
126
|
+
|
127
|
+
it 'returns errors for non-DateTime values' do
|
128
|
+
errors = schema.validate(start_time: "2024-01-01")
|
129
|
+
expect(errors).to contain_exactly("Start time must be a DateTime")
|
130
|
+
end
|
131
|
+
|
132
|
+
it 'can be required using presence validation' do
|
133
|
+
schema.validates :start_time, presence: true
|
134
|
+
errors = schema.validate(start_time: nil)
|
135
|
+
expect(errors).to contain_exactly("Start time can't be blank")
|
136
|
+
end
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
describe '#validate' do
|
141
|
+
let(:schema) do
|
142
|
+
described_class.new.tap do |s|
|
143
|
+
s.string(:name)
|
144
|
+
s.integer(:age)
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
it 'returns no errors when all values match their field types' do
|
149
|
+
errors = schema.validate(name: "John", age: 30)
|
150
|
+
expect(errors).to be_empty
|
151
|
+
end
|
152
|
+
|
153
|
+
it 'returns errors for type mismatches' do
|
154
|
+
errors = schema.validate(name: 123, age: "30")
|
155
|
+
expect(errors).to contain_exactly(
|
156
|
+
"Name must be a String",
|
157
|
+
"Age must be an Integer"
|
158
|
+
)
|
159
|
+
end
|
160
|
+
|
161
|
+
context 'with boolean fields' do
|
162
|
+
let(:schema) do
|
163
|
+
described_class.new.tap do |s|
|
164
|
+
s.boolean(:active)
|
165
|
+
end
|
166
|
+
end
|
167
|
+
|
168
|
+
it 'accepts true values' do
|
169
|
+
errors = schema.validate(active: true)
|
170
|
+
expect(errors).to be_empty
|
171
|
+
end
|
172
|
+
|
173
|
+
it 'accepts false values' do
|
174
|
+
errors = schema.validate(active: false)
|
175
|
+
expect(errors).to be_empty
|
176
|
+
end
|
177
|
+
|
178
|
+
it 'accepts nil values' do
|
179
|
+
errors = schema.validate(active: nil)
|
180
|
+
expect(errors).to be_empty
|
181
|
+
end
|
182
|
+
|
183
|
+
it 'returns errors for non-boolean values' do
|
184
|
+
errors = schema.validate(active: 'true')
|
185
|
+
expect(errors).to contain_exactly("Active must be true or false")
|
186
|
+
|
187
|
+
errors = schema.validate(active: 1)
|
188
|
+
expect(errors).to contain_exactly("Active must be true or false")
|
189
|
+
end
|
190
|
+
|
191
|
+
it 'can be required using presence validation' do
|
192
|
+
schema.validates :active, presence: true
|
193
|
+
errors = schema.validate(active: nil)
|
194
|
+
expect(errors).to contain_exactly("Active can't be blank")
|
195
|
+
end
|
196
|
+
end
|
197
|
+
|
198
|
+
context 'with string fields' do
|
199
|
+
let(:schema) do
|
200
|
+
described_class.new.tap do |s|
|
201
|
+
s.string(:name)
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
it 'accepts string values' do
|
206
|
+
errors = schema.validate(name: "John")
|
207
|
+
expect(errors).to be_empty
|
208
|
+
end
|
209
|
+
|
210
|
+
it 'accepts nil values' do
|
211
|
+
errors = schema.validate(name: nil)
|
212
|
+
expect(errors).to be_empty
|
213
|
+
end
|
214
|
+
|
215
|
+
it 'returns errors for non-string values' do
|
216
|
+
errors = schema.validate(name: 123)
|
217
|
+
expect(errors).to contain_exactly("Name must be a String")
|
218
|
+
end
|
219
|
+
|
220
|
+
it 'can be required using presence validation' do
|
221
|
+
schema.validates :name, presence: true
|
222
|
+
errors = schema.validate(name: nil)
|
223
|
+
expect(errors).to contain_exactly("Name can't be blank")
|
224
|
+
end
|
225
|
+
end
|
226
|
+
|
227
|
+
context 'with integer fields' do
|
228
|
+
let(:schema) do
|
229
|
+
described_class.new.tap do |s|
|
230
|
+
s.integer(:age)
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
it 'accepts integer values' do
|
235
|
+
errors = schema.validate(age: 30)
|
236
|
+
expect(errors).to be_empty
|
237
|
+
end
|
238
|
+
|
239
|
+
it 'accepts nil values' do
|
240
|
+
errors = schema.validate(age: nil)
|
241
|
+
expect(errors).to be_empty
|
242
|
+
end
|
243
|
+
|
244
|
+
it 'returns errors for non-integer values' do
|
245
|
+
errors = schema.validate(age: "30")
|
246
|
+
expect(errors).to contain_exactly("Age must be an Integer")
|
247
|
+
end
|
248
|
+
|
249
|
+
it 'can be required using presence validation' do
|
250
|
+
schema.validates :age, presence: true
|
251
|
+
errors = schema.validate(age: nil)
|
252
|
+
expect(errors).to contain_exactly("Age can't be blank")
|
253
|
+
end
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
describe 'ActiveModel validations' do
|
258
|
+
let(:schema) do
|
259
|
+
described_class.new.tap do |s|
|
260
|
+
s.string(:name)
|
261
|
+
s.integer(:age)
|
262
|
+
|
263
|
+
s.validates :name, presence: true
|
264
|
+
s.validates :age, numericality: { greater_than: 0 }
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
it 'generates a class that includes ActiveModel::Validations' do
|
269
|
+
schema_model = schema.schema_model
|
270
|
+
expect(schema_model.ancestors).to include(ActiveModel::Validations)
|
271
|
+
end
|
272
|
+
|
273
|
+
it 'adds accessors for defined fields' do
|
274
|
+
schema_model = schema.schema_model
|
275
|
+
instance = schema_model.new
|
276
|
+
instance.name = "John"
|
277
|
+
instance.age = 30
|
278
|
+
expect(instance.name).to eq("John")
|
279
|
+
expect(instance.age).to eq(30)
|
280
|
+
end
|
281
|
+
|
282
|
+
it 'validates presence' do
|
283
|
+
schema_model = schema.schema_model
|
284
|
+
instance = schema_model.new
|
285
|
+
expect(instance.valid?).to be false
|
286
|
+
expect(instance.errors.full_messages).to include("Name can't be blank")
|
287
|
+
end
|
288
|
+
|
289
|
+
it 'validates numericality' do
|
290
|
+
schema_model = schema.schema_model
|
291
|
+
instance = schema_model.new(name: "John", age: 0)
|
292
|
+
expect(instance.valid?).to be false
|
293
|
+
expect(instance.errors.full_messages).to include("Age must be greater than 0")
|
294
|
+
end
|
295
|
+
|
296
|
+
it 'returns true for valid instances' do
|
297
|
+
schema_model = schema.schema_model
|
298
|
+
instance = schema_model.new(name: "John", age: 30)
|
299
|
+
expect(instance.valid?).to be true
|
300
|
+
end
|
301
|
+
end
|
302
|
+
end
|