lluminary 0.1.4 → 0.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7a5f888a309d13934cdfdd0c6aa60f78fd66f063d518b86765614970f7275e3d
4
- data.tar.gz: f8793e6f888d7dec7037b92d31d64032328436f66b439aaa34bf82440773a9d9
3
+ metadata.gz: 20a5892da2184571fc6e1a681fc5ed81143c87a5abafc745249b0a28d5ef4f3a
4
+ data.tar.gz: 984b6467b9ac1a82af4107efbd72a4ae14ab05cae8e3838120dd0252ea0a5dc0
5
5
  SHA512:
6
- metadata.gz: 8e1d020d9a1f29ab98221b474ed75a0b8800f7f3e102ed5a31ad141b8f11a6ad414c0b4c649f392912468a5d95f323b0e4c214312d04e5d35281d88fbbf41949
7
- data.tar.gz: d38bf71f39c468e332ebdf5d7d64bf2bb353a8d4cbbc911e116614684d0eb790270fa683d7325d0f4858e11e12c5a6db19308524bf18921aede6338033bab3da
6
+ metadata.gz: 85ad7a6f048fc3bad3ca587a638da18e5026dce9a7f5068e551170c5531f26a1b5da83b6fe193339dbeae1eaf087d50283d96742e5f0ace04905a9eb8abb73b1
7
+ data.tar.gz: 0c501c6277d1b58114998b005b02d0a6ab10aec14ed8f21026d8c682a20c9d09692ee818cb24252c2befd88f99aa578fa4302b880d6f8ed67b05ae8b56b94b87
@@ -25,11 +25,11 @@ module Lluminary
25
25
 
26
26
  #{output_preamble}
27
27
 
28
- #{format_field_descriptions(task.class.output_fields)}
28
+ #{format_fields_descriptions(task.class.output_fields)}
29
29
 
30
30
  #{json_preamble}
31
31
 
32
- #{format_json_example(task.class.output_fields)}
32
+ #{generate_example_json_object(task.class.output_fields)}
33
33
  PROMPT
34
34
  end
35
35
 
@@ -46,30 +46,142 @@ module Lluminary
46
46
  "Your response must be ONLY this JSON object:"
47
47
  end
48
48
 
49
- def format_field_descriptions(fields)
49
+ def format_fields_descriptions(fields)
50
50
  fields
51
- .map do |name, field|
52
- desc = "# #{name}"
53
- desc += "\nType: #{format_type(field)}"
51
+ .map { |name, field| format_field_description(name, field) }
52
+ .compact # Remove nil entries from skipped types
53
+ .join("\n\n")
54
+ end
54
55
 
55
- desc += "\nDescription: #{field[:description].chomp}" if field[
56
- :description
57
- ]
56
+ def format_field_description(name, field, name_for_example = nil)
57
+ case field[:type]
58
+ when :hash
59
+ format_hash_description(name, field, name_for_example)
60
+ when :array
61
+ format_array_description(name, field, name_for_example)
62
+ else
63
+ format_simple_field_description(name, field, name_for_example)
64
+ end
65
+ end
66
+
67
+ def format_hash_description(name, field, name_for_example = nil)
68
+ return nil unless field[:fields]
69
+
70
+ lines = build_field_description_lines(name, field, name_for_example)
71
+
72
+ # Add descriptions for each field in the hash
73
+ field[:fields].each do |subname, subfield|
74
+ lines << "\n#{format_field_description("#{name}.#{subname}", subfield, subname)}"
75
+ end
58
76
 
59
- if (validations = describe_validations(field[:validations]))
60
- desc += "\nValidations: #{validations}"
77
+ lines.join("\n")
78
+ end
79
+
80
+ # Helper to ensure consistent JSON formatting for examples
81
+ def format_json_for_examples(value)
82
+ case value
83
+ when Hash, Array
84
+ JSON.generate(value)
85
+ else
86
+ value.inspect
87
+ end
88
+ end
89
+
90
+ def format_simple_field_description(name, field, name_for_example = nil)
91
+ build_field_description_lines(name, field, name_for_example).join("\n")
92
+ end
93
+
94
+ def format_array_description(name, field, name_for_example = nil)
95
+ lines = build_field_description_lines(name, field, name_for_example)
96
+
97
+ if field[:element_type]
98
+ if field[:element_type][:type] == :array
99
+ # Create a nested array field by adding [] to the name
100
+ # and recursively call format_array_description
101
+ element_field = field[:element_type].dup
102
+ nested_description =
103
+ format_array_description("#{name}[]", element_field, "item")
104
+ lines << "\n#{nested_description}"
105
+ elsif field[:element_type][:type] == :hash &&
106
+ field[:element_type][:fields]
107
+ field[:element_type][:fields].each do |subname, subfield|
108
+ inner_field = {
109
+ type: subfield[:type],
110
+ description: subfield[:description]
111
+ }
112
+ inner_lines =
113
+ build_field_description_lines(
114
+ "#{name}[].#{subname}",
115
+ inner_field,
116
+ subname
117
+ )
118
+ lines << "\n#{inner_lines.join("\n")}"
61
119
  end
120
+ else
121
+ inner_field = {
122
+ type: field[:element_type][:type],
123
+ description: field[:element_type][:description]
124
+ }
125
+ inner_lines =
126
+ build_field_description_lines("#{name}[]", inner_field, "item")
127
+ lines << "\n#{inner_lines.join("\n")}"
128
+ end
129
+ end
130
+
131
+ lines.join("\n")
132
+ end
133
+
134
+ # Common method for building field description lines
135
+ def build_field_description_lines(name, field, name_for_example = nil)
136
+ lines = []
137
+ # Add field description
138
+ lines << "# #{name}"
139
+ lines << "Description: #{field[:description]}" if field[:description]
140
+ lines << "Type: #{format_type(field)}"
141
+
142
+ # Add validation info
143
+ if (validations = describe_validations(field))
144
+ lines << "Validations: #{validations}"
145
+ end
146
+
147
+ # Generate and add example
148
+ example_value =
149
+ generate_example_value(
150
+ name_for_example || name.to_s.split(".").last,
151
+ field
152
+ )
153
+ lines << "Example: #{format_json_for_examples(example_value)}"
154
+
155
+ lines
156
+ end
62
157
 
63
- desc += "\nExample: #{generate_example_value(name, field)}"
64
- desc
158
+ def format_type(field)
159
+ case field[:type]
160
+ when :datetime
161
+ "datetime in ISO8601 format"
162
+ when :array
163
+ if field[:element_type].nil?
164
+ "array"
165
+ elsif field[:element_type][:type] == :array
166
+ "array of arrays"
167
+ elsif field[:element_type][:type] == :datetime
168
+ "array of datetimes in ISO8601 format"
169
+ elsif field[:element_type][:type] == :hash
170
+ "array of objects"
171
+ else
172
+ "array of #{field[:element_type][:type]}s"
65
173
  end
66
- .join("\n\n")
174
+ when :hash
175
+ "object"
176
+ else
177
+ field[:type].to_s
178
+ end
67
179
  end
68
180
 
69
- def describe_validations(validations)
70
- return unless validations&.any?
181
+ def describe_validations(field)
182
+ return unless field[:validations]&.any?
71
183
 
72
- validations
184
+ field[:validations]
73
185
  .map do |options|
74
186
  case options.keys.first
75
187
  when :presence
@@ -81,7 +193,7 @@ module Lluminary
81
193
  when :format
82
194
  "must match format: #{options[:format][:with]}"
83
195
  when :length
84
- describe_length_validation(options[:length])
196
+ describe_length_validation(options[:length], field[:type])
85
197
  when :numericality
86
198
  describe_numericality_validation(options[:numericality])
87
199
  when :comparison
@@ -94,43 +206,29 @@ module Lluminary
94
206
  .join(", ")
95
207
  end
96
208
 
97
- def describe_length_validation(options)
209
+ def describe_length_validation(options, field_type = nil)
98
210
  descriptions = []
211
+ units = field_type == :array ? "elements" : "characters"
212
+
99
213
  if options[:minimum]
100
- descriptions << "must be at least #{options[:minimum]} characters"
214
+ descriptions << "must have at least #{options[:minimum]} #{units}"
101
215
  end
102
216
  if options[:maximum]
103
- descriptions << "must be at most #{options[:maximum]} characters"
217
+ descriptions << "must have at most #{options[:maximum]} #{units}"
104
218
  end
105
219
  if options[:is]
106
- descriptions << "must be exactly #{options[:is]} characters"
220
+ descriptions << "must have exactly #{options[:is]} #{units}"
107
221
  end
108
222
  if options[:in]
109
- descriptions << "must be between #{options[:in].min} and #{options[:in].max} characters"
223
+ descriptions << "must have between #{options[:in].min} and #{options[:in].max} #{units}"
110
224
  end
111
225
  descriptions.join(", ")
112
226
  end
113
227
 
114
228
  def describe_numericality_validation(options)
115
229
  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
230
+ descriptions.concat(describe_common_comparisons(options))
231
+
134
232
  if options[:in]
135
233
  descriptions << "must be in: #{options[:in].to_a.join(", ")}"
136
234
  end
@@ -140,6 +238,10 @@ module Lluminary
140
238
  end
141
239
 
142
240
  def describe_comparison_validation(options)
241
+ describe_common_comparisons(options).join(", ")
242
+ end
243
+
244
+ def describe_common_comparisons(options)
143
245
  descriptions = []
144
246
  if options[:greater_than]
145
247
  descriptions << "must be greater than #{options[:greater_than]}"
@@ -159,10 +261,10 @@ module Lluminary
159
261
  if options[:other_than]
160
262
  descriptions << "must be other than #{options[:other_than]}"
161
263
  end
162
- descriptions.join(", ")
264
+ descriptions
163
265
  end
164
266
 
165
- def format_json_example(fields)
267
+ def generate_example_json_object(fields)
166
268
  example =
167
269
  fields.each_with_object({}) do |(name, field), hash|
168
270
  hash[name] = generate_example_value(name, field)
@@ -170,26 +272,14 @@ module Lluminary
170
272
  JSON.pretty_generate(example)
171
273
  end
172
274
 
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
275
  def generate_example_value(name, field)
190
276
  case field[:type]
191
277
  when :string
192
- "your #{name} here"
278
+ if name == "item" # For items in arrays
279
+ "first #{name}"
280
+ else
281
+ "your #{name} here"
282
+ end
193
283
  when :integer
194
284
  0
195
285
  when :datetime
@@ -200,6 +290,8 @@ module Lluminary
200
290
  0.0
201
291
  when :array
202
292
  generate_array_example(name, field)
293
+ when :hash
294
+ generate_hash_example(name, field)
203
295
  end
204
296
  end
205
297
 
@@ -208,11 +300,7 @@ module Lluminary
208
300
 
209
301
  case field[:element_type][:type]
210
302
  when :string
211
- [
212
- "first #{name.to_s.singularize}",
213
- "second #{name.to_s.singularize}",
214
- "..."
215
- ]
303
+ ["first #{name.to_s.singularize}", "second #{name.to_s.singularize}"]
216
304
  when :integer
217
305
  [1, 2, 3]
218
306
  when :float
@@ -226,8 +314,20 @@ module Lluminary
226
314
  inner_example = generate_array_example("item", field[:element_type])
227
315
  [inner_example, inner_example]
228
316
  else
229
- [["..."], ["..."]]
317
+ [[], []]
230
318
  end
319
+ when :hash
320
+ example =
321
+ generate_hash_example(name.to_s.singularize, field[:element_type])
322
+ [example, example]
323
+ end
324
+ end
325
+
326
+ def generate_hash_example(name, field)
327
+ return {} unless field[:fields]
328
+
329
+ field[:fields].each_with_object({}) do |(subname, subfield), hash|
330
+ hash[subname] = generate_example_value(subname, subfield)
231
331
  end
232
332
  end
233
333
  end
@@ -42,6 +42,21 @@ module Lluminary
42
42
  @fields[name] = field
43
43
  end
44
44
 
45
+ def hash(name, description: nil, &block)
46
+ unless block
47
+ raise ArgumentError, "Hash fields must be defined with a block"
48
+ end
49
+
50
+ nested_schema = Schema.new
51
+ nested_schema.instance_eval(&block)
52
+
53
+ @fields[name] = {
54
+ type: :hash,
55
+ description: description,
56
+ fields: nested_schema.fields
57
+ }
58
+ end
59
+
45
60
  attr_reader :fields
46
61
 
47
62
  def validates(*args, **options)
@@ -100,6 +115,17 @@ module Lluminary
100
115
  ) if block
101
116
  field
102
117
  end
118
+
119
+ def hash(description: nil, &block)
120
+ unless block
121
+ raise ArgumentError, "Hash fields must be defined with a block"
122
+ end
123
+
124
+ nested_schema = Schema.new
125
+ nested_schema.instance_eval(&block)
126
+
127
+ { type: :hash, description: description, fields: nested_schema.fields }
128
+ end
103
129
  end
104
130
  end
105
131
  end
@@ -21,6 +21,12 @@ module Lluminary
21
21
 
22
22
  def self.build(fields:, validations:)
23
23
  Class.new(self) do
24
+ class << self
25
+ attr_accessor :schema_fields
26
+ end
27
+
28
+ self.schema_fields = fields
29
+
24
30
  # Add accessors for each field
25
31
  fields.each_key do |name|
26
32
  define_method(name) { @attributes[name.to_s] }
@@ -29,7 +35,7 @@ module Lluminary
29
35
 
30
36
  # Add raw_response field and validation
31
37
  define_method(:raw_response) { @attributes["raw_response"] }
32
- define_method(:raw_response=) do |value|
38
+ define_method("raw_response=") do |value|
33
39
  @attributes["raw_response"] = value
34
40
  end
35
41
 
@@ -41,92 +47,44 @@ module Lluminary
41
47
  record.errors.add(:raw_response, "must be valid JSON")
42
48
  end
43
49
  end
44
- end
45
-
46
- # Add type validations
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
50
 
100
51
  record.attributes.each do |name, value|
101
52
  next if name == "raw_response"
102
53
  next if value.nil?
103
54
 
104
- field = fields[name.to_sym]
55
+ field = self.class.schema_fields[name.to_sym]
105
56
  next unless field
106
57
 
107
58
  case field[:type]
59
+ when :hash
60
+ validate_hash_field(record, name.to_s.capitalize, value, field)
61
+ when :array
62
+ validate_array_field(
63
+ record,
64
+ name.to_s.capitalize,
65
+ value,
66
+ field[:element_type]
67
+ )
108
68
  when :string
109
69
  unless value.is_a?(String)
110
- record.errors.add(name, "must be a String")
70
+ record.errors.add(name.to_s.capitalize, "must be a String")
111
71
  end
112
72
  when :integer
113
73
  unless value.is_a?(Integer)
114
- record.errors.add(name, "must be an Integer")
74
+ record.errors.add(name.to_s.capitalize, "must be an Integer")
115
75
  end
116
76
  when :boolean
117
77
  unless [true, false].include?(value)
118
- record.errors.add(name, "must be true or false")
78
+ record.errors.add(name.to_s.capitalize, "must be true or false")
119
79
  end
120
80
  when :float
121
81
  unless value.is_a?(Float)
122
- record.errors.add(name, "must be a float")
82
+ record.errors.add(name.to_s.capitalize, "must be a float")
123
83
  end
124
84
  when :datetime
125
85
  unless value.is_a?(DateTime)
126
- record.errors.add(name, "must be a DateTime")
86
+ record.errors.add(name.to_s.capitalize, "must be a DateTime")
127
87
  end
128
- when :array
129
- validate_array_field(record, name, value, field[:element_type])
130
88
  end
131
89
  end
132
90
  end
@@ -138,6 +96,118 @@ module Lluminary
138
96
  define_singleton_method(:model_name) do
139
97
  ActiveModel::Name.new(self, nil, "SchemaModel")
140
98
  end
99
+
100
+ private
101
+
102
+ def validate_hash_field(
103
+ record,
104
+ name,
105
+ value,
106
+ field_definition,
107
+ path = nil
108
+ )
109
+ field_name = path || name
110
+
111
+ unless value.is_a?(Hash)
112
+ record.errors.add(field_name, "must be a Hash")
113
+ return
114
+ end
115
+
116
+ field_definition[:fields].each do |key, field|
117
+ current_path = path ? "#{path}[#{key}]" : "#{field_name}[#{key}]"
118
+ # Try both string and symbol keys
119
+ field_value = value[key.to_s] || value[key.to_sym]
120
+
121
+ next if field_value.nil?
122
+
123
+ case field[:type]
124
+ when :hash
125
+ validate_hash_field(record, key, field_value, field, current_path)
126
+ when :array
127
+ validate_array_field(
128
+ record,
129
+ key,
130
+ field_value,
131
+ field[:element_type],
132
+ current_path
133
+ )
134
+ when :string
135
+ unless field_value.is_a?(String)
136
+ record.errors.add(current_path, "must be a String")
137
+ end
138
+ when :integer
139
+ unless field_value.is_a?(Integer)
140
+ record.errors.add(current_path, "must be an Integer")
141
+ end
142
+ when :boolean
143
+ unless [true, false].include?(field_value)
144
+ record.errors.add(current_path, "must be true or false")
145
+ end
146
+ when :float
147
+ unless field_value.is_a?(Float)
148
+ record.errors.add(current_path, "must be a float")
149
+ end
150
+ when :datetime
151
+ unless field_value.is_a?(DateTime)
152
+ record.errors.add(current_path, "must be a DateTime")
153
+ end
154
+ end
155
+ end
156
+ end
157
+
158
+ def validate_array_field(record, name, value, element_type, path = nil)
159
+ field_name = path || name
160
+
161
+ unless value.is_a?(Array)
162
+ record.errors.add(field_name, "must be an Array")
163
+ return
164
+ end
165
+
166
+ return unless element_type # untyped array
167
+
168
+ value.each_with_index do |element, index|
169
+ current_path = "#{field_name}[#{index}]"
170
+
171
+ case element_type[:type]
172
+ when :hash
173
+ validate_hash_field(
174
+ record,
175
+ name,
176
+ element,
177
+ element_type,
178
+ current_path
179
+ )
180
+ when :array
181
+ validate_array_field(
182
+ record,
183
+ name,
184
+ element,
185
+ element_type[:element_type],
186
+ current_path
187
+ )
188
+ when :string
189
+ unless element.is_a?(String)
190
+ record.errors.add(current_path, "must be a String")
191
+ end
192
+ when :integer
193
+ unless element.is_a?(Integer)
194
+ record.errors.add(current_path, "must be an Integer")
195
+ end
196
+ when :boolean
197
+ unless [true, false].include?(element)
198
+ record.errors.add(current_path, "must be true or false")
199
+ end
200
+ when :float
201
+ unless element.is_a?(Float)
202
+ record.errors.add(current_path, "must be a float")
203
+ end
204
+ when :datetime
205
+ unless element.is_a?(DateTime)
206
+ record.errors.add(current_path, "must be a DateTime")
207
+ end
208
+ end
209
+ end
210
+ end
141
211
  end
142
212
  end
143
213
  end