lluminary 0.1.2 → 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 +1 -6
- 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/providers/base.rb +15 -2
- data/lib/lluminary/providers/bedrock.rb +25 -7
- data/lib/lluminary/providers/openai.rb +14 -4
- data/lib/lluminary/providers/test.rb +6 -0
- data/lib/lluminary/schema.rb +51 -0
- data/lib/lluminary/schema_model.rb +54 -0
- data/lib/lluminary/task.rb +11 -77
- data/lib/lluminary.rb +4 -2
- data/spec/examples/meal_suggester_spec.rb +64 -0
- 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 +39 -0
- data/spec/lluminary/providers/openai_spec.rb +32 -0
- data/spec/lluminary/providers/test_spec.rb +25 -0
- data/spec/lluminary/schema_model_spec.rb +77 -0
- data/spec/lluminary/schema_spec.rb +146 -0
- data/spec/lluminary/task_spec.rb +18 -550
- data/spec/spec_helper.rb +1 -0
- metadata +44 -9
- data/lib/lluminary/field_description.rb +0 -153
- data/spec/lluminary/field_description_spec.rb +0 -34
- data/spec/lluminary/providers/base_spec.rb +0 -20
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 77729a2c4dc59717c0fd5c8fa8cf21df682bbaeff5c66e501f0e409cb8ac16c4
|
4
|
+
data.tar.gz: 46aa2188fcad140a7e7dfa2cc3d4bd5008d1bd5afbf70920bc3b211eb9018480
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: ed78898c62ab28d52136d470f5ccf398b3d5dcce8643af51348834877b3561ae3ca5b0ca1e7d12fbab6d0a8b677e7599c393dd9f678af970da5d3d5f01da9e24
|
7
|
+
data.tar.gz: 961371925c55006aedfe7b89d043aa10411f90b26fe1c67be356c2b81ebcc618417eb0fa1ff1cdf5dcd7344adb0e75b208d82e0ae256e0497c178c6f6d3569f9
|
data/lib/lluminary/config.rb
CHANGED
@@ -3,12 +3,7 @@
|
|
3
3
|
module Lluminary
|
4
4
|
# Configuration class for Lluminary framework.
|
5
5
|
# Handles global settings and provider configurations.
|
6
|
-
|
7
|
-
# @example Setting up configuration
|
8
|
-
# Lluminary.configure do |config|
|
9
|
-
# config.provider = :openai
|
10
|
-
# config.api_key = "your-api-key"
|
11
|
-
# end
|
6
|
+
|
12
7
|
class Config
|
13
8
|
def initialize
|
14
9
|
@providers = {}
|
@@ -0,0 +1,235 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lluminary
|
4
|
+
module Models
|
5
|
+
# Base class for all LLM models.
|
6
|
+
# Defines the interface that all model classes must implement and provides
|
7
|
+
# default prompt formatting behavior.
|
8
|
+
class Base
|
9
|
+
# Checks if this model is compatible with a given provider
|
10
|
+
# @param provider_name [Symbol] The name of the provider to check
|
11
|
+
# @return [Boolean]
|
12
|
+
def compatible_with?(provider_name)
|
13
|
+
raise NotImplementedError, "Subclasses must implement #compatible_with?"
|
14
|
+
end
|
15
|
+
|
16
|
+
# Returns the name of the model
|
17
|
+
# @return [String]
|
18
|
+
def name
|
19
|
+
raise NotImplementedError, "Subclasses must implement #name"
|
20
|
+
end
|
21
|
+
|
22
|
+
def format_prompt(task)
|
23
|
+
<<~PROMPT
|
24
|
+
#{task.task_prompt.chomp}
|
25
|
+
|
26
|
+
#{output_preamble}
|
27
|
+
|
28
|
+
#{format_field_descriptions(task.class.output_fields)}
|
29
|
+
|
30
|
+
#{json_preamble}
|
31
|
+
|
32
|
+
#{format_json_example(task.class.output_fields)}
|
33
|
+
PROMPT
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def output_preamble
|
39
|
+
<<~PREAMBLE.chomp
|
40
|
+
You must respond with ONLY a valid JSON object. Do not include any other text, explanations, or formatting.
|
41
|
+
The JSON object must contain the following fields:
|
42
|
+
PREAMBLE
|
43
|
+
end
|
44
|
+
|
45
|
+
def json_preamble
|
46
|
+
"Your response must be ONLY this JSON object:"
|
47
|
+
end
|
48
|
+
|
49
|
+
def format_field_descriptions(fields)
|
50
|
+
fields
|
51
|
+
.map do |name, field|
|
52
|
+
desc = "# #{name}"
|
53
|
+
desc += "\nType: #{format_type(field)}"
|
54
|
+
|
55
|
+
desc += "\nDescription: #{field[:description].chomp}" if field[
|
56
|
+
:description
|
57
|
+
]
|
58
|
+
|
59
|
+
if (validations = describe_validations(field[:validations]))
|
60
|
+
desc += "\nValidations: #{validations}"
|
61
|
+
end
|
62
|
+
|
63
|
+
desc += "\nExample: #{generate_example_value(name, field)}"
|
64
|
+
desc
|
65
|
+
end
|
66
|
+
.join("\n\n")
|
67
|
+
end
|
68
|
+
|
69
|
+
def describe_validations(validations)
|
70
|
+
return unless validations&.any?
|
71
|
+
|
72
|
+
validations
|
73
|
+
.map do |options|
|
74
|
+
case options.keys.first
|
75
|
+
when :presence
|
76
|
+
"must be present"
|
77
|
+
when :inclusion
|
78
|
+
"must be one of: #{options[:inclusion][:in].join(", ")}"
|
79
|
+
when :exclusion
|
80
|
+
"must not be one of: #{options[:exclusion][:in].join(", ")}"
|
81
|
+
when :format
|
82
|
+
"must match format: #{options[:format][:with]}"
|
83
|
+
when :length
|
84
|
+
describe_length_validation(options[:length])
|
85
|
+
when :numericality
|
86
|
+
describe_numericality_validation(options[:numericality])
|
87
|
+
when :comparison
|
88
|
+
describe_comparison_validation(options[:comparison])
|
89
|
+
when :absence
|
90
|
+
"must be absent"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
.compact
|
94
|
+
.join(", ")
|
95
|
+
end
|
96
|
+
|
97
|
+
def describe_length_validation(options)
|
98
|
+
descriptions = []
|
99
|
+
if options[:minimum]
|
100
|
+
descriptions << "must be at least #{options[:minimum]} characters"
|
101
|
+
end
|
102
|
+
if options[:maximum]
|
103
|
+
descriptions << "must be at most #{options[:maximum]} characters"
|
104
|
+
end
|
105
|
+
if options[:is]
|
106
|
+
descriptions << "must be exactly #{options[:is]} characters"
|
107
|
+
end
|
108
|
+
if options[:in]
|
109
|
+
descriptions << "must be between #{options[:in].min} and #{options[:in].max} characters"
|
110
|
+
end
|
111
|
+
descriptions.join(", ")
|
112
|
+
end
|
113
|
+
|
114
|
+
def describe_numericality_validation(options)
|
115
|
+
descriptions = []
|
116
|
+
if options[:greater_than]
|
117
|
+
descriptions << "must be greater than #{options[:greater_than]}"
|
118
|
+
end
|
119
|
+
if options[:greater_than_or_equal_to]
|
120
|
+
descriptions << "must be greater than or equal to #{options[:greater_than_or_equal_to]}"
|
121
|
+
end
|
122
|
+
if options[:equal_to]
|
123
|
+
descriptions << "must be equal to #{options[:equal_to]}"
|
124
|
+
end
|
125
|
+
if options[:less_than]
|
126
|
+
descriptions << "must be less than #{options[:less_than]}"
|
127
|
+
end
|
128
|
+
if options[:less_than_or_equal_to]
|
129
|
+
descriptions << "must be less than or equal to #{options[:less_than_or_equal_to]}"
|
130
|
+
end
|
131
|
+
if options[:other_than]
|
132
|
+
descriptions << "must be other than #{options[:other_than]}"
|
133
|
+
end
|
134
|
+
if options[:in]
|
135
|
+
descriptions << "must be in: #{options[:in].to_a.join(", ")}"
|
136
|
+
end
|
137
|
+
descriptions << "must be odd" if options[:odd]
|
138
|
+
descriptions << "must be even" if options[:even]
|
139
|
+
descriptions.join(", ")
|
140
|
+
end
|
141
|
+
|
142
|
+
def describe_comparison_validation(options)
|
143
|
+
descriptions = []
|
144
|
+
if options[:greater_than]
|
145
|
+
descriptions << "must be greater than #{options[:greater_than]}"
|
146
|
+
end
|
147
|
+
if options[:greater_than_or_equal_to]
|
148
|
+
descriptions << "must be greater than or equal to #{options[:greater_than_or_equal_to]}"
|
149
|
+
end
|
150
|
+
if options[:equal_to]
|
151
|
+
descriptions << "must be equal to #{options[:equal_to]}"
|
152
|
+
end
|
153
|
+
if options[:less_than]
|
154
|
+
descriptions << "must be less than #{options[:less_than]}"
|
155
|
+
end
|
156
|
+
if options[:less_than_or_equal_to]
|
157
|
+
descriptions << "must be less than or equal to #{options[:less_than_or_equal_to]}"
|
158
|
+
end
|
159
|
+
if options[:other_than]
|
160
|
+
descriptions << "must be other than #{options[:other_than]}"
|
161
|
+
end
|
162
|
+
descriptions.join(", ")
|
163
|
+
end
|
164
|
+
|
165
|
+
def format_json_example(fields)
|
166
|
+
example =
|
167
|
+
fields.each_with_object({}) do |(name, field), hash|
|
168
|
+
hash[name] = generate_example_value(name, field)
|
169
|
+
end
|
170
|
+
JSON.pretty_generate(example)
|
171
|
+
end
|
172
|
+
|
173
|
+
def format_type(field)
|
174
|
+
type = field[:type]
|
175
|
+
case type
|
176
|
+
when :datetime
|
177
|
+
"datetime in ISO8601 format"
|
178
|
+
when :array
|
179
|
+
if field[:element_type]
|
180
|
+
"array of #{format_type(field[:element_type])}"
|
181
|
+
else
|
182
|
+
"array"
|
183
|
+
end
|
184
|
+
else
|
185
|
+
type.to_s
|
186
|
+
end
|
187
|
+
end
|
188
|
+
|
189
|
+
def generate_example_value(name, field)
|
190
|
+
case field[:type]
|
191
|
+
when :string
|
192
|
+
"your #{name} here"
|
193
|
+
when :integer
|
194
|
+
0
|
195
|
+
when :datetime
|
196
|
+
"2024-01-01T12:00:00+00:00"
|
197
|
+
when :boolean
|
198
|
+
true
|
199
|
+
when :float
|
200
|
+
0.0
|
201
|
+
when :array
|
202
|
+
generate_array_example(name, field)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
206
|
+
def generate_array_example(name, field)
|
207
|
+
return [] unless field[:element_type]
|
208
|
+
|
209
|
+
case field[:element_type][:type]
|
210
|
+
when :string
|
211
|
+
[
|
212
|
+
"first #{name.to_s.singularize}",
|
213
|
+
"second #{name.to_s.singularize}",
|
214
|
+
"..."
|
215
|
+
]
|
216
|
+
when :integer
|
217
|
+
[1, 2, 3]
|
218
|
+
when :float
|
219
|
+
[1.0, 2.0, 3.0]
|
220
|
+
when :boolean
|
221
|
+
[true, false, true]
|
222
|
+
when :datetime
|
223
|
+
%w[2024-01-01T12:00:00+00:00 2024-01-02T12:00:00+00:00]
|
224
|
+
when :array
|
225
|
+
if field[:element_type][:element_type]
|
226
|
+
inner_example = generate_array_example("item", field[:element_type])
|
227
|
+
[inner_example, inner_example]
|
228
|
+
else
|
229
|
+
[["..."], ["..."]]
|
230
|
+
end
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Lluminary
|
6
|
+
module Models
|
7
|
+
module Bedrock
|
8
|
+
class AmazonNovaProV1 < Lluminary::Models::Bedrock::Base
|
9
|
+
NAME = "amazon.nova-pro-v1"
|
10
|
+
VERSIONS = %w[0].freeze
|
11
|
+
CONTEXT_WINDOWS = %w[24k 300k].freeze
|
12
|
+
|
13
|
+
def compatible_with?(provider_name)
|
14
|
+
provider_name == :bedrock
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "base"
|
4
|
+
|
5
|
+
module Lluminary
|
6
|
+
module Models
|
7
|
+
module Bedrock
|
8
|
+
class AnthropicClaudeInstantV1 < Lluminary::Models::Bedrock::Base
|
9
|
+
NAME = "anthropic.claude-instant-v1"
|
10
|
+
|
11
|
+
def compatible_with?(provider_name)
|
12
|
+
provider_name == :bedrock
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lluminary
|
4
|
+
module Models
|
5
|
+
module Bedrock
|
6
|
+
# TODO: test me
|
7
|
+
class Base < Lluminary::Models::Base
|
8
|
+
VERSIONS = [].freeze
|
9
|
+
CONTEXT_WINDOWS = [].freeze
|
10
|
+
|
11
|
+
def default_version
|
12
|
+
self.class::VERSIONS.last
|
13
|
+
end
|
14
|
+
|
15
|
+
def default_context_window
|
16
|
+
nil
|
17
|
+
end
|
18
|
+
|
19
|
+
def name
|
20
|
+
[
|
21
|
+
self.class::NAME,
|
22
|
+
default_version,
|
23
|
+
default_context_window
|
24
|
+
].compact.join(":")
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Lluminary
|
4
|
+
module Models
|
5
|
+
module OpenAi
|
6
|
+
# Model class for OpenAI's GPT-3.5 Turbo
|
7
|
+
class Gpt35Turbo < Base
|
8
|
+
NAME = "gpt-3.5-turbo"
|
9
|
+
|
10
|
+
def compatible_with?(provider_name)
|
11
|
+
provider_name == :openai
|
12
|
+
end
|
13
|
+
|
14
|
+
def name
|
15
|
+
NAME
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -5,15 +5,28 @@ module Lluminary
|
|
5
5
|
# Base class for all LLM providers.
|
6
6
|
# Defines the interface that all providers must implement.
|
7
7
|
class Base
|
8
|
+
# The symbolic name of the provider. Must be overridden by subclasses.
|
9
|
+
NAME = :base
|
10
|
+
|
8
11
|
attr_reader :config
|
9
12
|
|
10
|
-
def initialize(**
|
11
|
-
@config =
|
13
|
+
def initialize(**config_overrides)
|
14
|
+
@config = default_provider_config.merge(config_overrides)
|
12
15
|
end
|
13
16
|
|
14
17
|
def call(prompt, task)
|
15
18
|
raise NotImplementedError, "Subclasses must implement #call"
|
16
19
|
end
|
20
|
+
|
21
|
+
def models
|
22
|
+
raise NotImplementedError, "Subclasses must implement #models"
|
23
|
+
end
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def default_provider_config
|
28
|
+
Lluminary.config.provider_config(self.class::NAME)
|
29
|
+
end
|
17
30
|
end
|
18
31
|
end
|
19
32
|
end
|
@@ -1,22 +1,22 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
require "aws-sdk-bedrockruntime"
|
3
|
+
require "aws-sdk-bedrock"
|
3
4
|
require "json"
|
4
5
|
require_relative "../provider_error"
|
5
6
|
|
6
|
-
require "pry-byebug"
|
7
|
-
|
8
7
|
module Lluminary
|
9
8
|
module Providers
|
10
9
|
# Provider for AWS Bedrock models.
|
11
10
|
# Implements the Base provider interface for AWS Bedrock's API.
|
12
11
|
class Bedrock < Base
|
13
|
-
|
12
|
+
NAME = :bedrock
|
13
|
+
DEFAULT_MODEL = Models::Bedrock::AnthropicClaudeInstantV1
|
14
14
|
|
15
15
|
attr_reader :client, :config
|
16
16
|
|
17
|
-
def initialize(**
|
17
|
+
def initialize(**config_overrides)
|
18
18
|
super
|
19
|
-
@config = {
|
19
|
+
@config = { model: DEFAULT_MODEL }.merge(config)
|
20
20
|
|
21
21
|
@client =
|
22
22
|
Aws::BedrockRuntime::Client.new(
|
@@ -31,8 +31,8 @@ module Lluminary
|
|
31
31
|
|
32
32
|
def call(prompt, _task)
|
33
33
|
response =
|
34
|
-
|
35
|
-
model_id:
|
34
|
+
client.converse(
|
35
|
+
model_id: model.name,
|
36
36
|
messages: [{ role: "user", content: [{ text: prompt }] }]
|
37
37
|
)
|
38
38
|
|
@@ -48,6 +48,24 @@ module Lluminary
|
|
48
48
|
end
|
49
49
|
}
|
50
50
|
end
|
51
|
+
|
52
|
+
def model
|
53
|
+
@model ||= config[:model].new
|
54
|
+
end
|
55
|
+
|
56
|
+
def models
|
57
|
+
models_client =
|
58
|
+
Aws::Bedrock::Client.new(
|
59
|
+
region: config[:region],
|
60
|
+
credentials:
|
61
|
+
Aws::Credentials.new(
|
62
|
+
config[:access_key_id],
|
63
|
+
config[:secret_access_key]
|
64
|
+
)
|
65
|
+
)
|
66
|
+
response = models_client.list_foundation_models
|
67
|
+
response.foundation_models.map(&:model_id)
|
68
|
+
end
|
51
69
|
end
|
52
70
|
end
|
53
71
|
end
|
@@ -8,11 +8,12 @@ module Lluminary
|
|
8
8
|
# Provider for OpenAI's GPT models.
|
9
9
|
# Implements the Base provider interface for OpenAI's API.
|
10
10
|
class OpenAI < Base
|
11
|
-
|
11
|
+
NAME = :openai
|
12
|
+
DEFAULT_MODEL = Models::OpenAi::Gpt35Turbo
|
12
13
|
|
13
14
|
attr_reader :client, :config
|
14
15
|
|
15
|
-
def initialize(**
|
16
|
+
def initialize(**config_overrides)
|
16
17
|
super
|
17
18
|
@config = { model: DEFAULT_MODEL }.merge(config)
|
18
19
|
@client = ::OpenAI::Client.new(access_token: config[:api_key])
|
@@ -20,9 +21,9 @@ module Lluminary
|
|
20
21
|
|
21
22
|
def call(prompt, _task)
|
22
23
|
response =
|
23
|
-
|
24
|
+
client.chat(
|
24
25
|
parameters: {
|
25
|
-
model:
|
26
|
+
model: model.class::NAME,
|
26
27
|
messages: [{ role: "user", content: prompt }],
|
27
28
|
response_format: {
|
28
29
|
type: "json_object"
|
@@ -42,6 +43,15 @@ module Lluminary
|
|
42
43
|
end
|
43
44
|
}
|
44
45
|
end
|
46
|
+
|
47
|
+
def model
|
48
|
+
@model ||= config[:model].new
|
49
|
+
end
|
50
|
+
|
51
|
+
def models
|
52
|
+
response = @client.models.list
|
53
|
+
response["data"].map { |model| model["id"] }
|
54
|
+
end
|
45
55
|
end
|
46
56
|
end
|
47
57
|
end
|
@@ -5,12 +5,18 @@ module Lluminary
|
|
5
5
|
# Test provider for development and testing.
|
6
6
|
# Returns predefined responses for testing purposes.
|
7
7
|
class Test < Base
|
8
|
+
NAME = :test
|
9
|
+
|
8
10
|
def call(_prompt, task)
|
9
11
|
response = generate_response(task.class.output_fields)
|
10
12
|
raw_response = JSON.pretty_generate(response).gsub(/\n\s*/, "")
|
11
13
|
{ raw: raw_response, parsed: JSON.parse(raw_response) }
|
12
14
|
end
|
13
15
|
|
16
|
+
def model
|
17
|
+
@model ||= Lluminary::Models::Base.new
|
18
|
+
end
|
19
|
+
|
14
20
|
private
|
15
21
|
|
16
22
|
def generate_response(fields)
|
data/lib/lluminary/schema.rb
CHANGED
@@ -31,10 +31,30 @@ module Lluminary
|
|
31
31
|
@fields[name] = { type: :datetime, description: description }
|
32
32
|
end
|
33
33
|
|
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
|
43
|
+
end
|
44
|
+
|
34
45
|
attr_reader :fields
|
35
46
|
|
36
47
|
def validates(*args, **options)
|
37
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
|
38
58
|
end
|
39
59
|
|
40
60
|
def validations_for(field_name)
|
@@ -50,5 +70,36 @@ module Lluminary
|
|
50
70
|
instance = schema_model.new(values)
|
51
71
|
instance.valid? ? [] : instance.errors.full_messages
|
52
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
|
53
104
|
end
|
54
105
|
end
|
@@ -45,6 +45,58 @@ module Lluminary
|
|
45
45
|
|
46
46
|
# Add type validations
|
47
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
|
+
|
48
100
|
record.attributes.each do |name, value|
|
49
101
|
next if name == "raw_response"
|
50
102
|
next if value.nil?
|
@@ -73,6 +125,8 @@ module Lluminary
|
|
73
125
|
unless value.is_a?(DateTime)
|
74
126
|
record.errors.add(name, "must be a DateTime")
|
75
127
|
end
|
128
|
+
when :array
|
129
|
+
validate_array_field(record, name, value, field[:element_type])
|
76
130
|
end
|
77
131
|
end
|
78
132
|
end
|