lluminary 0.1.4 → 0.2.1

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: cacc0edbbc80b69f4c95d6b9888707651dffc5189432920b1c814abed095ea01
4
+ data.tar.gz: 647f143f0be6053b7ca80868e2d6bd5058cfee8fae0d0e12a51ad59f8e8d5233
5
5
  SHA512:
6
- metadata.gz: 8e1d020d9a1f29ab98221b474ed75a0b8800f7f3e102ed5a31ad141b8f11a6ad414c0b4c649f392912468a5d95f323b0e4c214312d04e5d35281d88fbbf41949
7
- data.tar.gz: d38bf71f39c468e332ebdf5d7d64bf2bb353a8d4cbbc911e116614684d0eb790270fa683d7325d0f4858e11e12c5a6db19308524bf18921aede6338033bab3da
6
+ metadata.gz: 63dce0789e7b175ad324d7de8f7bbd4c90da575e314f14cbd3c4ab9c6b3cab5b942cd279a15b545ac322f267bda221efd3988c6b7e9c7f2f930f1de0c1af87ac
7
+ data.tar.gz: 30a523b37b850f6603e466e05c49ceb8eeb03fb8ee81c3b237b97b175ace6082e0e97ace99b210e2807fb1d9feab2520a4b95753d5690e7fb46d02df1faa0389
@@ -25,11 +25,13 @@ 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
+ #{format_additional_validations(task.class.output_custom_validations)}
31
+
30
32
  #{json_preamble}
31
33
 
32
- #{format_json_example(task.class.output_fields)}
34
+ #{generate_example_json_object(task.class.output_fields)}
33
35
  PROMPT
34
36
  end
35
37
 
@@ -46,30 +48,142 @@ module Lluminary
46
48
  "Your response must be ONLY this JSON object:"
47
49
  end
48
50
 
49
- def format_field_descriptions(fields)
51
+ def format_fields_descriptions(fields)
50
52
  fields
51
- .map do |name, field|
52
- desc = "# #{name}"
53
- desc += "\nType: #{format_type(field)}"
53
+ .map { |name, field| format_field_description(name, field) }
54
+ .compact # Remove nil entries from skipped types
55
+ .join("\n\n")
56
+ end
57
+
58
+ def format_field_description(name, field, name_for_example = nil)
59
+ case field[:type]
60
+ when :hash
61
+ format_hash_description(name, field, name_for_example)
62
+ when :array
63
+ format_array_description(name, field, name_for_example)
64
+ else
65
+ format_simple_field_description(name, field, name_for_example)
66
+ end
67
+ end
68
+
69
+ def format_hash_description(name, field, name_for_example = nil)
70
+ return nil unless field[:fields]
71
+
72
+ lines = build_field_description_lines(name, field, name_for_example)
54
73
 
55
- desc += "\nDescription: #{field[:description].chomp}" if field[
56
- :description
57
- ]
74
+ # Add descriptions for each field in the hash
75
+ field[:fields].each do |subname, subfield|
76
+ lines << "\n#{format_field_description("#{name}.#{subname}", subfield, subname)}"
77
+ end
78
+
79
+ lines.join("\n")
80
+ end
58
81
 
59
- if (validations = describe_validations(field[:validations]))
60
- desc += "\nValidations: #{validations}"
82
+ # Helper to ensure consistent JSON formatting for examples
83
+ def format_json_for_examples(value)
84
+ case value
85
+ when Hash, Array
86
+ JSON.generate(value)
87
+ else
88
+ value.inspect
89
+ end
90
+ end
91
+
92
+ def format_simple_field_description(name, field, name_for_example = nil)
93
+ build_field_description_lines(name, field, name_for_example).join("\n")
94
+ end
95
+
96
+ def format_array_description(name, field, name_for_example = nil)
97
+ lines = build_field_description_lines(name, field, name_for_example)
98
+
99
+ if field[:element_type]
100
+ if field[:element_type][:type] == :array
101
+ # Create a nested array field by adding [] to the name
102
+ # and recursively call format_array_description
103
+ element_field = field[:element_type].dup
104
+ nested_description =
105
+ format_array_description("#{name}[]", element_field, "item")
106
+ lines << "\n#{nested_description}"
107
+ elsif field[:element_type][:type] == :hash &&
108
+ field[:element_type][:fields]
109
+ field[:element_type][:fields].each do |subname, subfield|
110
+ inner_field = {
111
+ type: subfield[:type],
112
+ description: subfield[:description]
113
+ }
114
+ inner_lines =
115
+ build_field_description_lines(
116
+ "#{name}[].#{subname}",
117
+ inner_field,
118
+ subname
119
+ )
120
+ lines << "\n#{inner_lines.join("\n")}"
61
121
  end
122
+ else
123
+ inner_field = {
124
+ type: field[:element_type][:type],
125
+ description: field[:element_type][:description]
126
+ }
127
+ inner_lines =
128
+ build_field_description_lines("#{name}[]", inner_field, "item")
129
+ lines << "\n#{inner_lines.join("\n")}"
130
+ end
131
+ end
132
+
133
+ lines.join("\n")
134
+ end
135
+
136
+ # Common method for building field description lines
137
+ def build_field_description_lines(name, field, name_for_example = nil)
138
+ lines = []
139
+ # Add field description
140
+ lines << "# #{name}"
141
+ lines << "Description: #{field[:description]}" if field[:description]
142
+ lines << "Type: #{format_type(field)}"
62
143
 
63
- desc += "\nExample: #{generate_example_value(name, field)}"
64
- desc
144
+ # Add validation info
145
+ if (validations = describe_validations(field))
146
+ lines << "Validations: #{validations}"
147
+ end
148
+
149
+ # Generate and add example
150
+ example_value =
151
+ generate_example_value(
152
+ name_for_example || name.to_s.split(".").last,
153
+ field
154
+ )
155
+ lines << "Example: #{format_json_for_examples(example_value)}"
156
+
157
+ lines
158
+ end
159
+
160
+ def format_type(field)
161
+ case field[:type]
162
+ when :datetime
163
+ "datetime in ISO8601 format"
164
+ when :array
165
+ if field[:element_type].nil?
166
+ "array"
167
+ elsif field[:element_type][:type] == :array
168
+ "array of arrays"
169
+ elsif field[:element_type][:type] == :datetime
170
+ "array of datetimes in ISO8601 format"
171
+ elsif field[:element_type][:type] == :hash
172
+ "array of objects"
173
+ else
174
+ "array of #{field[:element_type][:type]}s"
65
175
  end
66
- .join("\n\n")
176
+ when :hash
177
+ "object"
178
+ else
179
+ field[:type].to_s
180
+ end
67
181
  end
68
182
 
69
- def describe_validations(validations)
70
- return unless validations&.any?
183
+ def describe_validations(field)
184
+ return unless field[:validations]&.any?
71
185
 
72
- validations
186
+ field[:validations]
73
187
  .map do |options|
74
188
  case options.keys.first
75
189
  when :presence
@@ -81,7 +195,7 @@ module Lluminary
81
195
  when :format
82
196
  "must match format: #{options[:format][:with]}"
83
197
  when :length
84
- describe_length_validation(options[:length])
198
+ describe_length_validation(options[:length], field[:type])
85
199
  when :numericality
86
200
  describe_numericality_validation(options[:numericality])
87
201
  when :comparison
@@ -94,43 +208,29 @@ module Lluminary
94
208
  .join(", ")
95
209
  end
96
210
 
97
- def describe_length_validation(options)
211
+ def describe_length_validation(options, field_type = nil)
98
212
  descriptions = []
213
+ units = field_type == :array ? "elements" : "characters"
214
+
99
215
  if options[:minimum]
100
- descriptions << "must be at least #{options[:minimum]} characters"
216
+ descriptions << "must have at least #{options[:minimum]} #{units}"
101
217
  end
102
218
  if options[:maximum]
103
- descriptions << "must be at most #{options[:maximum]} characters"
219
+ descriptions << "must have at most #{options[:maximum]} #{units}"
104
220
  end
105
221
  if options[:is]
106
- descriptions << "must be exactly #{options[:is]} characters"
222
+ descriptions << "must have exactly #{options[:is]} #{units}"
107
223
  end
108
224
  if options[:in]
109
- descriptions << "must be between #{options[:in].min} and #{options[:in].max} characters"
225
+ descriptions << "must have between #{options[:in].min} and #{options[:in].max} #{units}"
110
226
  end
111
227
  descriptions.join(", ")
112
228
  end
113
229
 
114
230
  def describe_numericality_validation(options)
115
231
  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
232
+ descriptions.concat(describe_common_comparisons(options))
233
+
134
234
  if options[:in]
135
235
  descriptions << "must be in: #{options[:in].to_a.join(", ")}"
136
236
  end
@@ -140,6 +240,10 @@ module Lluminary
140
240
  end
141
241
 
142
242
  def describe_comparison_validation(options)
243
+ describe_common_comparisons(options).join(", ")
244
+ end
245
+
246
+ def describe_common_comparisons(options)
143
247
  descriptions = []
144
248
  if options[:greater_than]
145
249
  descriptions << "must be greater than #{options[:greater_than]}"
@@ -159,10 +263,10 @@ module Lluminary
159
263
  if options[:other_than]
160
264
  descriptions << "must be other than #{options[:other_than]}"
161
265
  end
162
- descriptions.join(", ")
266
+ descriptions
163
267
  end
164
268
 
165
- def format_json_example(fields)
269
+ def generate_example_json_object(fields)
166
270
  example =
167
271
  fields.each_with_object({}) do |(name, field), hash|
168
272
  hash[name] = generate_example_value(name, field)
@@ -170,26 +274,14 @@ module Lluminary
170
274
  JSON.pretty_generate(example)
171
275
  end
172
276
 
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
277
  def generate_example_value(name, field)
190
278
  case field[:type]
191
279
  when :string
192
- "your #{name} here"
280
+ if name == "item" # For items in arrays
281
+ "first #{name}"
282
+ else
283
+ "your #{name} here"
284
+ end
193
285
  when :integer
194
286
  0
195
287
  when :datetime
@@ -200,6 +292,8 @@ module Lluminary
200
292
  0.0
201
293
  when :array
202
294
  generate_array_example(name, field)
295
+ when :hash
296
+ generate_hash_example(name, field)
203
297
  end
204
298
  end
205
299
 
@@ -208,11 +302,7 @@ module Lluminary
208
302
 
209
303
  case field[:element_type][:type]
210
304
  when :string
211
- [
212
- "first #{name.to_s.singularize}",
213
- "second #{name.to_s.singularize}",
214
- "..."
215
- ]
305
+ ["first #{name.to_s.singularize}", "second #{name.to_s.singularize}"]
216
306
  when :integer
217
307
  [1, 2, 3]
218
308
  when :float
@@ -226,10 +316,31 @@ module Lluminary
226
316
  inner_example = generate_array_example("item", field[:element_type])
227
317
  [inner_example, inner_example]
228
318
  else
229
- [["..."], ["..."]]
319
+ [[], []]
230
320
  end
321
+ when :hash
322
+ example =
323
+ generate_hash_example(name.to_s.singularize, field[:element_type])
324
+ [example, example]
325
+ end
326
+ end
327
+
328
+ def generate_hash_example(name, field)
329
+ return {} unless field[:fields]
330
+
331
+ field[:fields].each_with_object({}) do |(subname, subfield), hash|
332
+ hash[subname] = generate_example_value(subname, subfield)
231
333
  end
232
334
  end
335
+
336
+ def format_additional_validations(custom_validations)
337
+ descriptions = custom_validations.map { |v| v[:description] }.compact
338
+ return "" if descriptions.empty?
339
+
340
+ section = ["Additional Validations:"]
341
+ descriptions.each { |desc| section << "- #{desc}" }
342
+ "#{section.join("\n")}\n"
343
+ end
233
344
  end
234
345
  end
235
346
  end
@@ -9,6 +9,7 @@ module Lluminary
9
9
  def initialize
10
10
  @fields = {}
11
11
  @validations = []
12
+ @custom_validations = []
12
13
  end
13
14
 
14
15
  def string(name, description: nil)
@@ -42,7 +43,22 @@ module Lluminary
42
43
  @fields[name] = field
43
44
  end
44
45
 
45
- attr_reader :fields
46
+ def hash(name, description: nil, &block)
47
+ unless block
48
+ raise ArgumentError, "Hash fields must be defined with a block"
49
+ end
50
+
51
+ nested_schema = Schema.new
52
+ nested_schema.instance_eval(&block)
53
+
54
+ @fields[name] = {
55
+ type: :hash,
56
+ description: description,
57
+ fields: nested_schema.fields
58
+ }
59
+ end
60
+
61
+ attr_reader :fields, :custom_validations
46
62
 
47
63
  def validates(*args, **options)
48
64
  @validations << [args, options]
@@ -57,18 +73,21 @@ module Lluminary
57
73
  end
58
74
  end
59
75
 
76
+ def validate(method_name, description: nil)
77
+ @custom_validations << { method: method_name, description: description }
78
+ end
79
+
60
80
  def validations_for(field_name)
61
81
  @validations.select { |args, _| args.include?(field_name) }
62
82
  end
63
83
 
64
84
  def schema_model
65
85
  @schema_model ||=
66
- SchemaModel.build(fields: @fields, validations: @validations)
67
- end
68
-
69
- def validate(values)
70
- instance = schema_model.new(values)
71
- instance.valid? ? [] : instance.errors.full_messages
86
+ SchemaModel.build(
87
+ fields: @fields,
88
+ validations: @validations,
89
+ custom_validations: @custom_validations
90
+ )
72
91
  end
73
92
 
74
93
  # Internal class for defining array element types
@@ -100,6 +119,17 @@ module Lluminary
100
119
  ) if block
101
120
  field
102
121
  end
122
+
123
+ def hash(description: nil, &block)
124
+ unless block
125
+ raise ArgumentError, "Hash fields must be defined with a block"
126
+ end
127
+
128
+ nested_schema = Schema.new
129
+ nested_schema.instance_eval(&block)
130
+
131
+ { type: :hash, description: description, fields: nested_schema.fields }
132
+ end
103
133
  end
104
134
  end
105
135
  end