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
data/lib/lluminary/schema.rb
CHANGED
@@ -1,13 +1,10 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "active_model"
|
3
|
+
require_relative "schema_model"
|
3
4
|
|
4
5
|
module Lluminary
|
5
|
-
|
6
|
-
|
7
|
-
record.errors.add(:base, "Response must be valid JSON") unless value.is_a?(Hash)
|
8
|
-
end
|
9
|
-
end
|
10
|
-
|
6
|
+
# Represents a JSON schema for validating task inputs and outputs.
|
7
|
+
# Provides methods for defining and validating schemas.
|
11
8
|
class Schema
|
12
9
|
def initialize
|
13
10
|
@fields = {}
|
@@ -34,12 +31,30 @@ module Lluminary
|
|
34
31
|
@fields[name] = { type: :datetime, description: description }
|
35
32
|
end
|
36
33
|
|
37
|
-
def
|
38
|
-
|
34
|
+
def array(name, description: nil, &block)
|
35
|
+
field = { type: :array, description: description }
|
36
|
+
|
37
|
+
if block
|
38
|
+
element_schema = ArrayElementSchema.new
|
39
|
+
field[:element_type] = element_schema.instance_eval(&block)
|
40
|
+
end
|
41
|
+
|
42
|
+
@fields[name] = field
|
39
43
|
end
|
40
44
|
|
45
|
+
attr_reader :fields
|
46
|
+
|
41
47
|
def validates(*args, **options)
|
42
48
|
@validations << [args, options]
|
49
|
+
# Attach the validation to each field it applies to
|
50
|
+
args.each do |field_name|
|
51
|
+
field = @fields[field_name]
|
52
|
+
next unless field # Skip if field doesn't exist yet
|
53
|
+
|
54
|
+
field[:validations] ||= []
|
55
|
+
# Store each validation option separately
|
56
|
+
options.each { |key, value| field[:validations] << { key => value } }
|
57
|
+
end
|
43
58
|
end
|
44
59
|
|
45
60
|
def validations_for(field_name)
|
@@ -47,15 +62,44 @@ module Lluminary
|
|
47
62
|
end
|
48
63
|
|
49
64
|
def schema_model
|
50
|
-
@schema_model ||=
|
51
|
-
fields: @fields,
|
52
|
-
validations: @validations
|
53
|
-
)
|
65
|
+
@schema_model ||=
|
66
|
+
SchemaModel.build(fields: @fields, validations: @validations)
|
54
67
|
end
|
55
68
|
|
56
69
|
def validate(values)
|
57
70
|
instance = schema_model.new(values)
|
58
71
|
instance.valid? ? [] : instance.errors.full_messages
|
59
72
|
end
|
73
|
+
|
74
|
+
# Internal class for defining array element types
|
75
|
+
class ArrayElementSchema
|
76
|
+
def string(description: nil)
|
77
|
+
{ type: :string, description: description }
|
78
|
+
end
|
79
|
+
|
80
|
+
def integer(description: nil)
|
81
|
+
{ type: :integer, description: description }
|
82
|
+
end
|
83
|
+
|
84
|
+
def boolean(description: nil)
|
85
|
+
{ type: :boolean, description: description }
|
86
|
+
end
|
87
|
+
|
88
|
+
def float(description: nil)
|
89
|
+
{ type: :float, description: description }
|
90
|
+
end
|
91
|
+
|
92
|
+
def datetime(description: nil)
|
93
|
+
{ type: :datetime, description: description }
|
94
|
+
end
|
95
|
+
|
96
|
+
def array(description: nil, &block)
|
97
|
+
field = { type: :array, description: description }
|
98
|
+
field[:element_type] = ArrayElementSchema.new.instance_eval(
|
99
|
+
&block
|
100
|
+
) if block
|
101
|
+
field
|
102
|
+
end
|
103
|
+
end
|
60
104
|
end
|
61
|
-
end
|
105
|
+
end
|
@@ -1,6 +1,9 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "active_model"
|
2
3
|
|
3
4
|
module Lluminary
|
5
|
+
# Base class for models that use JSON schema validation.
|
6
|
+
# Provides ActiveModel integration and schema validation.
|
4
7
|
class SchemaModel
|
5
8
|
include ActiveModel::Validations
|
6
9
|
|
@@ -12,7 +15,7 @@ module Lluminary
|
|
12
15
|
|
13
16
|
def to_s
|
14
17
|
attrs = attributes.dup
|
15
|
-
attrs.delete(
|
18
|
+
attrs.delete("raw_response")
|
16
19
|
"#<#{self.class.name} #{attrs.inspect}>"
|
17
20
|
end
|
18
21
|
|
@@ -25,8 +28,10 @@ module Lluminary
|
|
25
28
|
end
|
26
29
|
|
27
30
|
# Add raw_response field and validation
|
28
|
-
define_method(:raw_response) { @attributes[
|
29
|
-
define_method(:raw_response=)
|
31
|
+
define_method(:raw_response) { @attributes["raw_response"] }
|
32
|
+
define_method(:raw_response=) do |value|
|
33
|
+
@attributes["raw_response"] = value
|
34
|
+
end
|
30
35
|
|
31
36
|
validate do |record|
|
32
37
|
if record.raw_response
|
@@ -40,8 +45,60 @@ module Lluminary
|
|
40
45
|
|
41
46
|
# Add type validations
|
42
47
|
validate do |record|
|
48
|
+
def validate_array_field(
|
49
|
+
record,
|
50
|
+
name,
|
51
|
+
value,
|
52
|
+
element_type,
|
53
|
+
path = nil
|
54
|
+
)
|
55
|
+
field_name = path || name
|
56
|
+
|
57
|
+
unless value.is_a?(Array)
|
58
|
+
record.errors.add(field_name, "must be an Array")
|
59
|
+
return
|
60
|
+
end
|
61
|
+
|
62
|
+
return unless element_type # untyped array
|
63
|
+
|
64
|
+
value.each_with_index do |element, index|
|
65
|
+
current_path = "#{field_name}[#{index}]"
|
66
|
+
|
67
|
+
case element_type[:type]
|
68
|
+
when :array
|
69
|
+
validate_array_field(
|
70
|
+
record,
|
71
|
+
name,
|
72
|
+
element,
|
73
|
+
element_type[:element_type],
|
74
|
+
current_path
|
75
|
+
)
|
76
|
+
when :string
|
77
|
+
unless element.is_a?(String)
|
78
|
+
record.errors.add(current_path, "must be a String")
|
79
|
+
end
|
80
|
+
when :integer
|
81
|
+
unless element.is_a?(Integer)
|
82
|
+
record.errors.add(current_path, "must be an Integer")
|
83
|
+
end
|
84
|
+
when :boolean
|
85
|
+
unless [true, false].include?(element)
|
86
|
+
record.errors.add(current_path, "must be true or false")
|
87
|
+
end
|
88
|
+
when :float
|
89
|
+
unless element.is_a?(Float)
|
90
|
+
record.errors.add(current_path, "must be a float")
|
91
|
+
end
|
92
|
+
when :datetime
|
93
|
+
unless element.is_a?(DateTime)
|
94
|
+
record.errors.add(current_path, "must be a DateTime")
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
43
100
|
record.attributes.each do |name, value|
|
44
|
-
next if name ==
|
101
|
+
next if name == "raw_response"
|
45
102
|
next if value.nil?
|
46
103
|
|
47
104
|
field = fields[name.to_sym]
|
@@ -57,7 +114,7 @@ module Lluminary
|
|
57
114
|
record.errors.add(name, "must be an Integer")
|
58
115
|
end
|
59
116
|
when :boolean
|
60
|
-
unless
|
117
|
+
unless [true, false].include?(value)
|
61
118
|
record.errors.add(name, "must be true or false")
|
62
119
|
end
|
63
120
|
when :float
|
@@ -68,14 +125,14 @@ module Lluminary
|
|
68
125
|
unless value.is_a?(DateTime)
|
69
126
|
record.errors.add(name, "must be a DateTime")
|
70
127
|
end
|
128
|
+
when :array
|
129
|
+
validate_array_field(record, name, value, field[:element_type])
|
71
130
|
end
|
72
131
|
end
|
73
132
|
end
|
74
133
|
|
75
134
|
# Add ActiveModel validations
|
76
|
-
validations.each
|
77
|
-
validates(*args, **options)
|
78
|
-
end
|
135
|
+
validations.each { |args, options| validates(*args, **options) }
|
79
136
|
|
80
137
|
# Set model name for error messages
|
81
138
|
define_singleton_method(:model_name) do
|
@@ -84,4 +141,4 @@ module Lluminary
|
|
84
141
|
end
|
85
142
|
end
|
86
143
|
end
|
87
|
-
end
|
144
|
+
end
|
data/lib/lluminary/task.rb
CHANGED
@@ -1,10 +1,15 @@
|
|
1
|
-
|
2
|
-
|
3
|
-
|
4
|
-
require_relative
|
5
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "ostruct"
|
3
|
+
require "json"
|
4
|
+
require_relative "schema"
|
5
|
+
require_relative "validation_error"
|
6
|
+
require_relative "models/base"
|
7
|
+
require_relative "models/openai/gpt35_turbo"
|
8
|
+
require_relative "models/bedrock/anthropic_claude_instant_v1"
|
6
9
|
|
7
10
|
module Lluminary
|
11
|
+
# Base class for all Lluminary tasks.
|
12
|
+
# Provides the core functionality for defining and running LLM-powered tasks.
|
8
13
|
class Task
|
9
14
|
class << self
|
10
15
|
def input_schema(&block)
|
@@ -18,25 +23,22 @@ module Lluminary
|
|
18
23
|
end
|
19
24
|
|
20
25
|
def use_provider(provider_name, **config)
|
21
|
-
provider_class =
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
# Merge global config with task-specific config
|
36
|
-
global_config = Lluminary.config.provider_config(provider_name)
|
37
|
-
merged_config = global_config.merge(config)
|
26
|
+
provider_class =
|
27
|
+
case provider_name
|
28
|
+
when :openai
|
29
|
+
require_relative "providers/openai"
|
30
|
+
Providers::OpenAI
|
31
|
+
when :test
|
32
|
+
require_relative "providers/test"
|
33
|
+
Providers::Test
|
34
|
+
when :bedrock
|
35
|
+
require_relative "providers/bedrock"
|
36
|
+
Providers::Bedrock
|
37
|
+
else
|
38
|
+
raise ArgumentError, "Unknown provider: #{provider_name}"
|
39
|
+
end
|
38
40
|
|
39
|
-
@provider = provider_class.new(**
|
41
|
+
@provider = provider_class.new(**config)
|
40
42
|
end
|
41
43
|
|
42
44
|
def call(input = {})
|
@@ -48,15 +50,14 @@ module Lluminary
|
|
48
50
|
end
|
49
51
|
|
50
52
|
def provider
|
51
|
-
@provider ||=
|
52
|
-
|
53
|
-
|
54
|
-
|
53
|
+
@provider ||=
|
54
|
+
begin
|
55
|
+
require_relative "providers/test"
|
56
|
+
Providers::Test.new
|
57
|
+
end
|
55
58
|
end
|
56
59
|
|
57
|
-
|
58
|
-
@provider = provider
|
59
|
-
end
|
60
|
+
attr_writer :provider
|
60
61
|
|
61
62
|
def input_fields
|
62
63
|
@input_schema&.fields || {}
|
@@ -98,7 +99,7 @@ module Lluminary
|
|
98
99
|
validate_input!
|
99
100
|
response = self.class.provider.call(prompt, self)
|
100
101
|
process_response(response)
|
101
|
-
|
102
|
+
|
102
103
|
self
|
103
104
|
end
|
104
105
|
|
@@ -107,17 +108,16 @@ module Lluminary
|
|
107
108
|
end
|
108
109
|
|
109
110
|
def validate_input!
|
110
|
-
|
111
|
-
|
112
|
-
end
|
111
|
+
return if @input.valid?
|
112
|
+
raise ValidationError, @input.errors.full_messages.join(", ")
|
113
113
|
end
|
114
114
|
|
115
115
|
def prompt
|
116
|
-
|
117
|
-
|
116
|
+
@prompt ||= self.class.provider.model.format_prompt(self)
|
117
|
+
end
|
118
118
|
|
119
|
-
|
120
|
-
|
119
|
+
def task_prompt
|
120
|
+
raise NotImplementedError, "Subclasses must implement task_prompt"
|
121
121
|
end
|
122
122
|
|
123
123
|
private
|
@@ -134,27 +134,36 @@ module Lluminary
|
|
134
134
|
# Merge the parsed response first, then validate
|
135
135
|
if @parsed_response.is_a?(Hash)
|
136
136
|
# Get datetime fields from schema
|
137
|
-
datetime_fields =
|
137
|
+
datetime_fields =
|
138
|
+
self
|
139
|
+
.class
|
140
|
+
.output_fields
|
141
|
+
.select { |_, field| field[:type] == :datetime }
|
142
|
+
.keys
|
138
143
|
|
139
144
|
# Convert datetime fields
|
140
145
|
converted_response = @parsed_response.dup
|
141
146
|
datetime_fields.each do |field_name|
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
147
|
+
unless converted_response.key?(field_name.to_s) &&
|
148
|
+
converted_response[field_name.to_s].is_a?(String)
|
149
|
+
next
|
150
|
+
end
|
151
|
+
begin
|
152
|
+
converted_response[field_name.to_s] = DateTime.parse(
|
153
|
+
converted_response[field_name.to_s]
|
154
|
+
)
|
155
|
+
rescue ArgumentError
|
156
|
+
# Leave as string, validation will fail
|
148
157
|
end
|
149
158
|
end
|
150
159
|
|
151
160
|
@output.attributes.merge!(converted_response)
|
152
161
|
end
|
153
|
-
|
162
|
+
|
154
163
|
# Validate after merging
|
155
164
|
@output.valid?
|
156
165
|
|
157
|
-
|
166
|
+
prompt
|
158
167
|
end
|
159
168
|
|
160
169
|
def define_input_methods
|
@@ -163,62 +172,12 @@ module Lluminary
|
|
163
172
|
end
|
164
173
|
end
|
165
174
|
|
166
|
-
def task_prompt
|
167
|
-
raise NotImplementedError, "Subclasses must implement task_prompt"
|
168
|
-
end
|
169
|
-
|
170
|
-
def json_schema_example
|
171
|
-
return "{}" if fields.empty?
|
172
|
-
|
173
|
-
<<~SCHEMA.chomp
|
174
|
-
You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
|
175
|
-
The JSON object must contain the following fields:
|
176
|
-
|
177
|
-
#{generate_field_descriptions}
|
178
|
-
|
179
|
-
Your response must be ONLY this JSON object:
|
180
|
-
#{example_json}
|
181
|
-
SCHEMA
|
182
|
-
end
|
183
|
-
|
184
|
-
def fields
|
185
|
-
@fields ||= self.class.output_fields
|
186
|
-
end
|
187
|
-
|
188
|
-
def generate_field_descriptions
|
189
|
-
fields.map do |name, field|
|
190
|
-
# Get validations for this field
|
191
|
-
validations = self.class.instance_variable_get(:@output_schema)&.validations_for(name) || []
|
192
|
-
field_with_validations = field.merge(validations: validations)
|
193
|
-
FieldDescription.new(name, field_with_validations).to_schema_s
|
194
|
-
end.join("\n\n")
|
195
|
-
end
|
196
|
-
|
197
|
-
def example_json
|
198
|
-
json = fields.each_with_object({}) do |(name, field), hash|
|
199
|
-
hash[name] = case field[:type]
|
200
|
-
when :string
|
201
|
-
"your #{name} here"
|
202
|
-
when :integer
|
203
|
-
0
|
204
|
-
when :datetime
|
205
|
-
"2024-01-01T12:00:00+00:00"
|
206
|
-
when :boolean
|
207
|
-
true
|
208
|
-
when :float
|
209
|
-
0.0
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
JSON.pretty_generate(json)
|
214
|
-
end
|
215
|
-
|
216
175
|
def to_result
|
217
176
|
Result.new(
|
218
177
|
raw_response: @output&.raw_response,
|
219
178
|
output: @parsed_response,
|
220
|
-
prompt:
|
179
|
+
prompt: prompt
|
221
180
|
)
|
222
181
|
end
|
223
182
|
end
|
224
|
-
end
|
183
|
+
end
|
data/lib/lluminary/version.rb
CHANGED
data/lib/lluminary.rb
CHANGED
@@ -1,10 +1,24 @@
|
|
1
|
-
|
2
|
-
require_relative 'lluminary/result'
|
3
|
-
require_relative 'lluminary/task'
|
4
|
-
require_relative 'lluminary/providers/base'
|
5
|
-
require_relative 'lluminary/providers/openai'
|
6
|
-
require_relative 'lluminary/config'
|
1
|
+
# frozen_string_literal: true
|
7
2
|
|
3
|
+
require_relative "lluminary/version"
|
4
|
+
require_relative "lluminary/result"
|
5
|
+
require_relative "lluminary/task"
|
6
|
+
# automatically require all providers
|
7
|
+
Dir[File.join(__dir__, "lluminary/providers/*.rb")].each { |file| require file }
|
8
|
+
# automatically require all models
|
9
|
+
Dir[File.join(__dir__, "lluminary/models/**/*.rb")].each { |file| require file }
|
10
|
+
require_relative "lluminary/config"
|
11
|
+
|
12
|
+
# Lluminary is a framework for building and running LLM-powered tasks.
|
13
|
+
# It provides a structured way to define tasks, their inputs and outputs,
|
14
|
+
# and handles the interaction with various LLM providers.
|
15
|
+
#
|
16
|
+
# @example Creating a simple task
|
17
|
+
# class MyTask < Lluminary::Task
|
18
|
+
# def run
|
19
|
+
# # Task implementation
|
20
|
+
# end
|
21
|
+
# end
|
8
22
|
module Lluminary
|
9
23
|
class << self
|
10
24
|
def config
|
@@ -14,5 +28,9 @@ module Lluminary
|
|
14
28
|
def configure
|
15
29
|
yield config
|
16
30
|
end
|
31
|
+
|
32
|
+
def reset_configuration
|
33
|
+
@config = Config.new
|
34
|
+
end
|
17
35
|
end
|
18
|
-
end
|
36
|
+
end
|
@@ -1,8 +1,11 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
require_relative "../../examples/analyze_text"
|
3
4
|
|
4
5
|
RSpec.describe AnalyzeText do
|
5
|
-
let(:text) {
|
6
|
+
let(:text) { <<~TEXT }
|
7
|
+
Ruby is a dynamic, open source programming language with a focus on simplicity and productivity. It has an elegant syntax that is natural to read and easy to write.
|
8
|
+
TEXT
|
6
9
|
|
7
10
|
it "analyzes text" do
|
8
11
|
result = described_class.call(text: text)
|
@@ -18,4 +21,4 @@ RSpec.describe AnalyzeText do
|
|
18
21
|
expect(json).to have_key("analysis")
|
19
22
|
expect(json["analysis"]).to eq(result.output.analysis)
|
20
23
|
end
|
21
|
-
end
|
24
|
+
end
|
@@ -1,42 +1,42 @@
|
|
1
|
-
|
2
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
require "spec_helper"
|
3
|
+
require_relative "../../examples/color_analyzer"
|
3
4
|
|
4
5
|
RSpec.describe ColorAnalyzer do
|
5
|
-
describe
|
6
|
+
describe "#call" do
|
6
7
|
it 'returns "red" for a description strongly suggesting the color red' do
|
7
|
-
result = described_class.call(
|
8
|
-
|
9
|
-
|
8
|
+
result = described_class.call(image_description: <<~DESCRIPTION)
|
9
|
+
A bright red sports car parked in front of a red brick building at sunset. The car's glossy red paint reflects the warm light, making it appear even more vibrant. A red stop sign stands nearby, and red roses bloom in a garden beside the building.
|
10
|
+
DESCRIPTION
|
10
11
|
|
11
12
|
expect(result.output.color_name).to eq("red")
|
12
13
|
expect(result.output.valid?).to be true
|
13
14
|
end
|
14
15
|
|
15
|
-
it
|
16
|
+
it "returns invalid output when description strongly suggests orange" do
|
16
17
|
# This should fail validation because "orange" is not a CSS Level 1 color
|
17
|
-
result = described_class.call(
|
18
|
-
|
19
|
-
|
18
|
+
result = described_class.call(image_description: <<~DESCRIPTION)
|
19
|
+
A field of ripe oranges under a bright orange sunset. The fruit glows with a warm orange hue, and the sky is painted in shades of orange and gold. Orange butterflies flutter among the trees, and orange flowers bloom throughout the scene.
|
20
|
+
DESCRIPTION
|
20
21
|
|
21
22
|
expect(result.output.valid?).to be false
|
22
|
-
expect(result.output.errors.full_messages).to include(
|
23
|
+
expect(result.output.errors.full_messages).to include(
|
24
|
+
"Color name must be a valid CSS level 1 color name"
|
25
|
+
)
|
23
26
|
end
|
24
27
|
|
25
|
-
it
|
26
|
-
expect {
|
27
|
-
|
28
|
-
|
29
|
-
)
|
30
|
-
}.to raise_error(Lluminary::ValidationError)
|
28
|
+
it "validates presence of image_description" do
|
29
|
+
expect { described_class.call!(image_description: "") }.to raise_error(
|
30
|
+
Lluminary::ValidationError
|
31
|
+
)
|
31
32
|
end
|
32
33
|
|
33
|
-
it
|
34
|
-
result =
|
35
|
-
image_description: "A bright red sports car"
|
36
|
-
)
|
34
|
+
it "validates that color_name is lowercase" do
|
35
|
+
result =
|
36
|
+
described_class.call(image_description: "A bright red sports car")
|
37
37
|
|
38
38
|
expect(result.output.valid?).to be true
|
39
39
|
expect(result.output.color_name).to eq(result.output.color_name.downcase)
|
40
40
|
end
|
41
41
|
end
|
42
|
-
end
|
42
|
+
end
|