lluminary 0.2.3 → 0.2.4

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: 187dc38fab8e09fd65ffb2db359e914f16dc79ace8ff9d058de01f547db0921c
4
- data.tar.gz: 6a9a08f01d36966529c3c97e1877834df7a59e41347f68ffcba4c37a56597a4e
3
+ metadata.gz: 43ca21be47208b82183b4156be99d1e9816ea1013b2ee0c5258df158b0f4f2c1
4
+ data.tar.gz: 4f835024abc67bcd6bb9dd108fd49431eec6a152f168e31592b2d5317b982889
5
5
  SHA512:
6
- metadata.gz: 202c4b2f46a3a0f35e978406055781fe3c85d390ca5e520955b8fad80734dbe6434a9034767f149141876a28a69e50e2636c0064d1119ede7e2314df7e49bac1
7
- data.tar.gz: f0c17df74edfc0db4b7eb97917355f6c32da8761fa294ddd030f86bdc0540f208ae5220700b2d1a752ad25a1665367f7bfc6f02e51df3fe76b4ed79a51bef46a
6
+ metadata.gz: 2cc7b7322ec931bf1d0c5783b3dfbd26bb3c7fff841d834cc120d271c6ecef3e22472ba278c5cee8932685cfe71c850950a0e6305b88c0d997df7b3ebc0fdca4
7
+ data.tar.gz: b9f95c68e583a7819bb4b37e36c0b40d6856ce9f4261aea7efe78018823e950471a306f223033747e32d25fb81790a64af1f9cec70ee102be4a2811ccfcdede4
@@ -175,6 +175,18 @@ module Lluminary
175
175
  end
176
176
  when :hash
177
177
  "object"
178
+ when :dictionary
179
+ if field[:value_type].nil?
180
+ "object"
181
+ elsif field[:value_type][:type] == :array
182
+ "object with array values"
183
+ elsif field[:value_type][:type] == :datetime
184
+ "object with datetime values in ISO8601 format"
185
+ elsif field[:value_type][:type] == :hash
186
+ "object with object values"
187
+ else
188
+ "object with #{field[:value_type][:type]} values"
189
+ end
178
190
  else
179
191
  field[:type].to_s
180
192
  end
@@ -294,6 +306,8 @@ module Lluminary
294
306
  generate_array_example(name, field)
295
307
  when :hash
296
308
  generate_hash_example(name, field)
309
+ when :dictionary
310
+ generate_dictionary_example(name, field)
297
311
  end
298
312
  end
299
313
 
@@ -333,6 +347,37 @@ module Lluminary
333
347
  end
334
348
  end
335
349
 
350
+ def generate_dictionary_example(name, field)
351
+ return {} unless field[:value_type]
352
+
353
+ case field[:value_type][:type]
354
+ when :string
355
+ { "some_key" => "first value", "other_key" => "second value" }
356
+ when :integer
357
+ { "some_key" => 1, "other_key" => 2 }
358
+ when :float
359
+ { "some_key" => 1.0, "other_key" => 2.0 }
360
+ when :boolean
361
+ { "some_key" => true, "other_key" => false }
362
+ when :datetime
363
+ {
364
+ "some_key" => "2024-01-01T12:00:00+00:00",
365
+ "other_key" => "2024-01-02T12:00:00+00:00"
366
+ }
367
+ when :array
368
+ if field[:value_type][:element_type]
369
+ inner_example = generate_array_example("item", field[:value_type])
370
+ { "some_key" => inner_example, "other_key" => inner_example }
371
+ else
372
+ { "some_key" => [], "other_key" => [] }
373
+ end
374
+ when :hash
375
+ example =
376
+ generate_hash_example(name.to_s.singularize, field[:value_type])
377
+ { "some_key" => example, "other_key" => example }
378
+ end
379
+ end
380
+
336
381
  def format_additional_validations(custom_validations)
337
382
  descriptions = custom_validations.map { |v| v[:description] }.compact
338
383
  return "" if descriptions.empty?
@@ -36,7 +36,7 @@ module Lluminary
36
36
  field = { type: :array, description: description }
37
37
 
38
38
  if block
39
- element_schema = ArrayElementSchema.new
39
+ element_schema = ElementTypeSchema.new
40
40
  field[:element_type] = element_schema.instance_eval(&block)
41
41
  end
42
42
 
@@ -58,6 +58,21 @@ module Lluminary
58
58
  }
59
59
  end
60
60
 
61
+ def dictionary(name, description: nil, &block)
62
+ unless block
63
+ raise ArgumentError, "Dictionary fields must be defined with a block"
64
+ end
65
+
66
+ element_schema = ElementTypeSchema.new
67
+ value_type = element_schema.instance_eval(&block)
68
+
69
+ @fields[name] = {
70
+ type: :dictionary,
71
+ description: description,
72
+ value_type: value_type
73
+ }
74
+ end
75
+
61
76
  attr_reader :fields, :custom_validations
62
77
 
63
78
  def validates(*args, **options)
@@ -90,8 +105,8 @@ module Lluminary
90
105
  )
91
106
  end
92
107
 
93
- # Internal class for defining array element types
94
- class ArrayElementSchema
108
+ # Internal class for defining element types for arrays and dictionaries (or any other container-type objects)
109
+ class ElementTypeSchema
95
110
  def string(description: nil)
96
111
  { type: :string, description: description }
97
112
  end
@@ -114,7 +129,7 @@ module Lluminary
114
129
 
115
130
  def array(description: nil, &block)
116
131
  field = { type: :array, description: description }
117
- field[:element_type] = ArrayElementSchema.new.instance_eval(
132
+ field[:element_type] = ElementTypeSchema.new.instance_eval(
118
133
  &block
119
134
  ) if block
120
135
  field
@@ -130,6 +145,17 @@ module Lluminary
130
145
 
131
146
  { type: :hash, description: description, fields: nested_schema.fields }
132
147
  end
148
+
149
+ def dictionary(description: nil, &block)
150
+ unless block
151
+ raise ArgumentError, "Dictionary fields must be defined with a block"
152
+ end
153
+
154
+ element_schema = ElementTypeSchema.new
155
+ value_type = element_schema.instance_eval(&block)
156
+
157
+ { type: :dictionary, description: description, value_type: value_type }
158
+ end
133
159
  end
134
160
  end
135
161
  end
@@ -71,14 +71,11 @@ module Lluminary
71
71
 
72
72
  case field[:type]
73
73
  when :hash
74
- validate_hash_field(record, name.to_s.capitalize, value, field)
74
+ validate_hash_field(record, name, value, field)
75
75
  when :array
76
- validate_array_field(
77
- record,
78
- name.to_s.capitalize,
79
- value,
80
- field[:element_type]
81
- )
76
+ validate_array_field(record, name, value, field[:element_type])
77
+ when :dictionary
78
+ validate_dictionary_field(record, name, value, field[:value_type])
82
79
  when :string
83
80
  unless value.is_a?(String)
84
81
  record.errors.add(name.to_s.capitalize, "must be a String")
@@ -113,6 +110,72 @@ module Lluminary
113
110
 
114
111
  private
115
112
 
113
+ def validate_dictionary_field(
114
+ record,
115
+ name,
116
+ value,
117
+ value_type,
118
+ path = nil
119
+ )
120
+ field_name = path || name
121
+
122
+ unless value.is_a?(Hash)
123
+ record.errors.add(field_name, "must be a Hash")
124
+ return
125
+ end
126
+
127
+ value.each do |key, val|
128
+ current_path = "#{field_name}[#{key}]"
129
+
130
+ case value_type[:type]
131
+ when :string
132
+ unless val.is_a?(String)
133
+ record.errors.add(current_path, "must be a String")
134
+ end
135
+ when :integer
136
+ unless val.is_a?(Integer)
137
+ record.errors.add(current_path, "must be an Integer")
138
+ end
139
+ when :boolean
140
+ unless [true, false].include?(val)
141
+ record.errors.add(current_path, "must be true or false")
142
+ end
143
+ when :float
144
+ unless val.is_a?(Float)
145
+ record.errors.add(current_path, "must be a float")
146
+ end
147
+ when :datetime
148
+ unless val.is_a?(DateTime)
149
+ record.errors.add(current_path, "must be a DateTime")
150
+ end
151
+ when :hash
152
+ validate_hash_field(
153
+ record,
154
+ current_path,
155
+ val,
156
+ value_type,
157
+ current_path
158
+ )
159
+ when :array
160
+ validate_array_field(
161
+ record,
162
+ current_path,
163
+ val,
164
+ value_type[:element_type],
165
+ current_path
166
+ )
167
+ when :dictionary
168
+ validate_dictionary_field(
169
+ record,
170
+ current_path,
171
+ val,
172
+ value_type[:value_type],
173
+ current_path
174
+ )
175
+ end
176
+ end
177
+ end
178
+
116
179
  def validate_hash_field(
117
180
  record,
118
181
  name,
@@ -145,6 +208,14 @@ module Lluminary
145
208
  field[:element_type],
146
209
  current_path
147
210
  )
211
+ when :dictionary
212
+ validate_dictionary_field(
213
+ record,
214
+ key,
215
+ field_value,
216
+ field[:value_type],
217
+ current_path
218
+ )
148
219
  when :string
149
220
  unless field_value.is_a?(String)
150
221
  record.errors.add(current_path, "must be a String")
@@ -199,6 +270,14 @@ module Lluminary
199
270
  element_type[:element_type],
200
271
  current_path
201
272
  )
273
+ when :dictionary
274
+ validate_dictionary_field(
275
+ record,
276
+ name,
277
+ element,
278
+ element_type[:value_type],
279
+ current_path
280
+ )
202
281
  when :string
203
282
  unless element.is_a?(String)
204
283
  record.errors.add(current_path, "must be a String")
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+ require "spec_helper"
3
+ require_relative "../../examples/text_emotion_analyzer"
4
+
5
+ RSpec.describe TextEmotionAnalyzer do
6
+ let(:sample_text) { <<~TEXT }
7
+ The sun was setting behind the mountains, casting long shadows across the valley.
8
+ Sarah felt a mix of emotions as she watched the last rays of light disappear.
9
+ There was a deep sense of peace, but also a tinge of sadness knowing this beautiful moment would soon be gone.
10
+ She smiled through her tears, grateful for the experience yet longing for it to last just a little longer.
11
+ TEXT
12
+
13
+ describe "input validation" do
14
+ it "accepts valid text input" do
15
+ expect { described_class.call!(text: sample_text) }.not_to raise_error
16
+ end
17
+
18
+ it "requires text to be present" do
19
+ expect { described_class.call!(text: "") }.to raise_error(
20
+ Lluminary::ValidationError
21
+ )
22
+ end
23
+ end
24
+
25
+ describe "output validation" do
26
+ let(:result) { described_class.call(text: sample_text) }
27
+
28
+ it "returns a dictionary of emotion scores" do
29
+ expect(result.output.emotion_scores).to be_a(Hash)
30
+ expect(result.output.emotion_scores).not_to be_empty
31
+ end
32
+
33
+ it "returns float scores between 0.0 and 1.0" do
34
+ result.output.emotion_scores.each do |emotion, score|
35
+ expect(score).to be_a(Float)
36
+ expect(score).to be_between(0.0, 1.0)
37
+ end
38
+ end
39
+
40
+ it "returns a dominant emotion" do
41
+ expect(result.output.dominant_emotion).to be_a(String)
42
+ expect(result.output.dominant_emotion).not_to be_empty
43
+ end
44
+
45
+ it "returns an analysis" do
46
+ expect(result.output.analysis).to be_a(String)
47
+ expect(result.output.analysis).not_to be_empty
48
+ end
49
+
50
+ it "returns valid JSON response" do
51
+ expect(result.output.raw_response).to be_a(String)
52
+ expect { JSON.parse(result.output.raw_response) }.not_to raise_error
53
+ json = JSON.parse(result.output.raw_response)
54
+ expect(json).to have_key("emotion_scores")
55
+ expect(json).to have_key("dominant_emotion")
56
+ expect(json).to have_key("analysis")
57
+ end
58
+ end
59
+
60
+ describe "emotion detection" do
61
+ it "detects multiple emotions in complex text" do
62
+ result = described_class.call(text: sample_text)
63
+ expect(result.output.emotion_scores.size).to be >= 2
64
+ end
65
+
66
+ it "identifies the highest scoring emotion as dominant" do
67
+ result = described_class.call(text: sample_text)
68
+ dominant_score =
69
+ result.output.emotion_scores[result.output.dominant_emotion]
70
+ result.output.emotion_scores.each do |emotion, score|
71
+ expect(score).to be <= dominant_score
72
+ end
73
+ end
74
+ end
75
+ end
@@ -1409,6 +1409,147 @@ RSpec.describe Lluminary::Models::Base do
1409
1409
  expect(prompt).to include(expected_json)
1410
1410
  end
1411
1411
  end
1412
+
1413
+ context "with dictionary fields" do
1414
+ it "formats dictionary field description correctly" do
1415
+ task_class.output_schema do
1416
+ dictionary :emotion_scores,
1417
+ description: "Scores for each detected emotion" do
1418
+ float
1419
+ end
1420
+ end
1421
+
1422
+ prompt = model.format_prompt(task)
1423
+
1424
+ expected_description = <<~DESCRIPTION.chomp
1425
+ # emotion_scores
1426
+ Description: Scores for each detected emotion
1427
+ Type: object with float values
1428
+ Example: {"some_key":1.0,"other_key":2.0}
1429
+ DESCRIPTION
1430
+
1431
+ expect(prompt).to include(expected_description)
1432
+ end
1433
+
1434
+ it "formats dictionary of arrays field description correctly" do
1435
+ task_class.output_schema do
1436
+ dictionary :categories, description: "Categories and their items" do
1437
+ array { string }
1438
+ end
1439
+ end
1440
+
1441
+ prompt = model.format_prompt(task)
1442
+
1443
+ expected_description = <<~DESCRIPTION.chomp
1444
+ # categories
1445
+ Description: Categories and their items
1446
+ Type: object with array values
1447
+ Example: {"some_key":["first item","second item"],"other_key":["first item","second item"]}
1448
+ DESCRIPTION
1449
+
1450
+ expect(prompt).to include(expected_description)
1451
+ end
1452
+
1453
+ it "formats dictionary of objects field description correctly" do
1454
+ task_class.output_schema do
1455
+ dictionary :users, description: "User profiles" do
1456
+ hash do
1457
+ string :name
1458
+ integer :age
1459
+ end
1460
+ end
1461
+ end
1462
+
1463
+ prompt = model.format_prompt(task)
1464
+
1465
+ expected_description = <<~DESCRIPTION.chomp
1466
+ # users
1467
+ Description: User profiles
1468
+ Type: object with object values
1469
+ Example: {"some_key":{"name":"your name here","age":0},"other_key":{"name":"your name here","age":0}}
1470
+ DESCRIPTION
1471
+
1472
+ expect(prompt).to include(expected_description)
1473
+ end
1474
+
1475
+ it "generates correct JSON example for dictionary of floats" do
1476
+ task_class.output_schema do
1477
+ dictionary :scores, description: "Scores for each item" do
1478
+ float
1479
+ end
1480
+ end
1481
+
1482
+ prompt = model.format_prompt(task)
1483
+
1484
+ expected_json = <<~JSON.chomp
1485
+ {
1486
+ "scores": {
1487
+ "some_key": 1.0,
1488
+ "other_key": 2.0
1489
+ }
1490
+ }
1491
+ JSON
1492
+
1493
+ expect(prompt).to include(expected_json)
1494
+ end
1495
+
1496
+ it "generates correct JSON example for dictionary of arrays" do
1497
+ task_class.output_schema do
1498
+ dictionary :categories, description: "Categories and their items" do
1499
+ array { string }
1500
+ end
1501
+ end
1502
+
1503
+ prompt = model.format_prompt(task)
1504
+
1505
+ expected_json = <<~JSON.chomp
1506
+ {
1507
+ "categories": {
1508
+ "some_key": [
1509
+ "first item",
1510
+ "second item"
1511
+ ],
1512
+ "other_key": [
1513
+ "first item",
1514
+ "second item"
1515
+ ]
1516
+ }
1517
+ }
1518
+ JSON
1519
+
1520
+ expect(prompt).to include(expected_json)
1521
+ end
1522
+
1523
+ it "generates correct JSON example for dictionary of objects" do
1524
+ task_class.output_schema do
1525
+ dictionary :users, description: "User profiles" do
1526
+ hash do
1527
+ string :name
1528
+ integer :age
1529
+ end
1530
+ end
1531
+ end
1532
+
1533
+ prompt = model.format_prompt(task)
1534
+
1535
+ expected_json = <<~JSON.chomp
1536
+ {
1537
+ "users": {
1538
+ "some_key": {
1539
+ "name": "your name here",
1540
+ "age": 0
1541
+ },
1542
+ "other_key": {
1543
+ "name": "your name here",
1544
+ "age": 0
1545
+ }
1546
+ }
1547
+ }
1548
+ JSON
1549
+
1550
+ expect(prompt).to include(expected_json)
1551
+ end
1552
+ end
1412
1553
  end
1413
1554
  end
1414
1555
  end
@@ -428,4 +428,238 @@ RSpec.describe Lluminary::SchemaModel do
428
428
  )
429
429
  end
430
430
  end
431
+
432
+ describe "dictionary type enforcement" do
433
+ let(:fields) do
434
+ {
435
+ tags: {
436
+ type: :dictionary,
437
+ description: nil,
438
+ value_type: {
439
+ type: :string,
440
+ description: nil
441
+ }
442
+ }
443
+ }
444
+ end
445
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
446
+
447
+ it "validates that value is a hash" do
448
+ instance = model_class.new(tags: "not a hash")
449
+ expect(instance.valid?).to be false
450
+ expect(instance.errors.full_messages).to contain_exactly(
451
+ "Tags must be a Hash"
452
+ )
453
+ end
454
+
455
+ it "validates dictionary value types" do
456
+ instance = model_class.new(tags: { "tag1" => "valid", "tag2" => 123 })
457
+ expect(instance.valid?).to be false
458
+ expect(instance.errors.full_messages).to contain_exactly(
459
+ "Tags[tag2] must be a String"
460
+ )
461
+ end
462
+
463
+ it "accepts valid dictionary values" do
464
+ instance =
465
+ model_class.new(tags: { "tag1" => "valid", "tag2" => "also valid" })
466
+ expect(instance.valid?).to be true
467
+ end
468
+ end
469
+
470
+ describe "nested dictionary validation" do
471
+ let(:fields) do
472
+ {
473
+ config: {
474
+ type: :hash,
475
+ description: nil,
476
+ fields: {
477
+ settings: {
478
+ type: :dictionary,
479
+ description: nil,
480
+ value_type: {
481
+ type: :integer,
482
+ description: nil
483
+ }
484
+ }
485
+ }
486
+ }
487
+ }
488
+ end
489
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
490
+
491
+ it "validates dictionaries inside hashes" do
492
+ instance =
493
+ model_class.new(
494
+ config: {
495
+ settings: {
496
+ "timeout" => 30,
497
+ "retries" => "3" # should be integer
498
+ }
499
+ }
500
+ )
501
+ expect(instance.valid?).to be false
502
+ expect(instance.errors.full_messages).to contain_exactly(
503
+ "Config[settings][retries] must be an Integer"
504
+ )
505
+ end
506
+ end
507
+
508
+ describe "dictionary of hashes validation" do
509
+ let(:fields) do
510
+ {
511
+ users: {
512
+ type: :dictionary,
513
+ description: nil,
514
+ value_type: {
515
+ type: :hash,
516
+ description: nil,
517
+ fields: {
518
+ name: {
519
+ type: :string,
520
+ description: nil
521
+ },
522
+ age: {
523
+ type: :integer,
524
+ description: nil
525
+ }
526
+ }
527
+ }
528
+ }
529
+ }
530
+ end
531
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
532
+
533
+ it "validates hashes inside dictionaries" do
534
+ instance =
535
+ model_class.new(
536
+ users: {
537
+ "user1" => {
538
+ name: "Alice",
539
+ age: 30
540
+ },
541
+ "user2" => {
542
+ name: 123,
543
+ age: "invalid"
544
+ } # name should be string, age should be integer
545
+ }
546
+ )
547
+ expect(instance.valid?).to be false
548
+ expect(instance.errors.full_messages).to contain_exactly(
549
+ "Users[user2][name] must be a String",
550
+ "Users[user2][age] must be an Integer"
551
+ )
552
+ end
553
+ end
554
+
555
+ describe "dictionary of arrays validation" do
556
+ let(:fields) do
557
+ {
558
+ categories: {
559
+ type: :dictionary,
560
+ description: nil,
561
+ value_type: {
562
+ type: :array,
563
+ description: nil,
564
+ element_type: {
565
+ type: :string,
566
+ description: nil
567
+ }
568
+ }
569
+ }
570
+ }
571
+ end
572
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
573
+
574
+ it "validates arrays inside dictionaries" do
575
+ instance =
576
+ model_class.new(
577
+ categories: {
578
+ "fruits" => %w[apple banana],
579
+ "numbers" => ["one", 2, "three"] # should all be strings
580
+ }
581
+ )
582
+ expect(instance.valid?).to be false
583
+ expect(instance.errors.full_messages).to contain_exactly(
584
+ "Categories[numbers][1] must be a String"
585
+ )
586
+ end
587
+ end
588
+
589
+ describe "array of dictionaries validation" do
590
+ let(:fields) do
591
+ {
592
+ configs: {
593
+ type: :array,
594
+ description: nil,
595
+ element_type: {
596
+ type: :dictionary,
597
+ description: nil,
598
+ value_type: {
599
+ type: :string,
600
+ description: nil
601
+ }
602
+ }
603
+ }
604
+ }
605
+ end
606
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
607
+
608
+ it "validates dictionaries inside arrays" do
609
+ instance =
610
+ model_class.new(
611
+ configs: [
612
+ { "key1" => "value1", "key2" => "value2" },
613
+ { "key3" => "value3", "key4" => 123 } # should be string
614
+ ]
615
+ )
616
+ expect(instance.valid?).to be false
617
+ expect(instance.errors.full_messages).to contain_exactly(
618
+ "Configs[1][key4] must be a String"
619
+ )
620
+ end
621
+ end
622
+
623
+ describe "hash with dictionary validation" do
624
+ let(:fields) do
625
+ {
626
+ config: {
627
+ type: :hash,
628
+ description: "Configuration",
629
+ fields: {
630
+ name: {
631
+ type: :string,
632
+ description: nil
633
+ },
634
+ settings: {
635
+ type: :dictionary,
636
+ description: nil,
637
+ value_type: {
638
+ type: :boolean,
639
+ description: nil
640
+ }
641
+ }
642
+ }
643
+ }
644
+ }
645
+ end
646
+ let(:model_class) { described_class.build(fields: fields, validations: []) }
647
+
648
+ it "validates dictionaries inside hashes" do
649
+ instance =
650
+ model_class.new(
651
+ config: {
652
+ name: "test",
653
+ settings: {
654
+ "enabled" => true,
655
+ "active" => "true" # should be boolean
656
+ }
657
+ }
658
+ )
659
+ expect(instance.valid?).to be false
660
+ expect(instance.errors.full_messages).to contain_exactly(
661
+ "Config[settings][active] must be true or false"
662
+ )
663
+ end
664
+ end
431
665
  end
@@ -294,6 +294,25 @@ RSpec.describe Lluminary::Schema do
294
294
  }
295
295
  )
296
296
  end
297
+
298
+ it "supports dictionaries inside arrays" do
299
+ schema.array(:configs) { dictionary { string } }
300
+
301
+ expect(schema.fields[:configs]).to eq(
302
+ {
303
+ type: :array,
304
+ description: nil,
305
+ element_type: {
306
+ type: :dictionary,
307
+ description: nil,
308
+ value_type: {
309
+ type: :string,
310
+ description: nil
311
+ }
312
+ }
313
+ }
314
+ )
315
+ end
297
316
  end
298
317
 
299
318
  describe "#hash" do
@@ -414,6 +433,210 @@ RSpec.describe Lluminary::Schema do
414
433
  }
415
434
  )
416
435
  end
436
+
437
+ it "supports dictionaries inside hashes" do
438
+ schema.hash(:config) do
439
+ string :name
440
+ dictionary :settings do
441
+ string
442
+ end
443
+ end
444
+
445
+ expect(schema.fields[:config]).to eq(
446
+ {
447
+ type: :hash,
448
+ description: nil,
449
+ fields: {
450
+ name: {
451
+ type: :string,
452
+ description: nil
453
+ },
454
+ settings: {
455
+ type: :dictionary,
456
+ description: nil,
457
+ value_type: {
458
+ type: :string,
459
+ description: nil
460
+ }
461
+ }
462
+ }
463
+ }
464
+ )
465
+ end
466
+ end
467
+
468
+ describe "#dictionary" do
469
+ it "adds a dictionary field to the schema" do
470
+ schema.dictionary(:tags) { string }
471
+ expect(schema.fields).to eq(
472
+ {
473
+ tags: {
474
+ type: :dictionary,
475
+ description: nil,
476
+ value_type: {
477
+ type: :string,
478
+ description: nil
479
+ }
480
+ }
481
+ }
482
+ )
483
+ end
484
+
485
+ it "adds a dictionary field with description" do
486
+ schema.dictionary(:tags, description: "A map of tags") { string }
487
+ expect(schema.fields).to eq(
488
+ {
489
+ tags: {
490
+ type: :dictionary,
491
+ description: "A map of tags",
492
+ value_type: {
493
+ type: :string,
494
+ description: nil
495
+ }
496
+ }
497
+ }
498
+ )
499
+ end
500
+
501
+ it "requires a name for the dictionary field" do
502
+ expect { schema.dictionary }.to raise_error(ArgumentError)
503
+ end
504
+
505
+ it "requires a block for dictionary fields" do
506
+ expect { schema.dictionary(:tags) }.to raise_error(
507
+ ArgumentError,
508
+ "Dictionary fields must be defined with a block"
509
+ )
510
+ end
511
+
512
+ it "accepts string type without a name" do
513
+ schema.dictionary(:tags) { string }
514
+ expect(schema.fields).to eq(
515
+ {
516
+ tags: {
517
+ type: :dictionary,
518
+ description: nil,
519
+ value_type: {
520
+ type: :string,
521
+ description: nil
522
+ }
523
+ }
524
+ }
525
+ )
526
+ end
527
+
528
+ it "accepts integer type without a name" do
529
+ schema.dictionary(:scores) { integer }
530
+ expect(schema.fields).to eq(
531
+ {
532
+ scores: {
533
+ type: :dictionary,
534
+ description: nil,
535
+ value_type: {
536
+ type: :integer,
537
+ description: nil
538
+ }
539
+ }
540
+ }
541
+ )
542
+ end
543
+
544
+ it "accepts boolean type without a name" do
545
+ schema.dictionary(:flags) { boolean }
546
+ expect(schema.fields).to eq(
547
+ {
548
+ flags: {
549
+ type: :dictionary,
550
+ description: nil,
551
+ value_type: {
552
+ type: :boolean,
553
+ description: nil
554
+ }
555
+ }
556
+ }
557
+ )
558
+ end
559
+
560
+ it "accepts float type without a name" do
561
+ schema.dictionary(:ratings) { float }
562
+ expect(schema.fields).to eq(
563
+ {
564
+ ratings: {
565
+ type: :dictionary,
566
+ description: nil,
567
+ value_type: {
568
+ type: :float,
569
+ description: nil
570
+ }
571
+ }
572
+ }
573
+ )
574
+ end
575
+
576
+ it "accepts datetime type without a name" do
577
+ schema.dictionary(:timestamps) { datetime }
578
+ expect(schema.fields).to eq(
579
+ {
580
+ timestamps: {
581
+ type: :dictionary,
582
+ description: nil,
583
+ value_type: {
584
+ type: :datetime,
585
+ description: nil
586
+ }
587
+ }
588
+ }
589
+ )
590
+ end
591
+
592
+ it "supports hashes as dictionary values" do
593
+ schema.dictionary(:users) do
594
+ hash do
595
+ string :name
596
+ integer :age
597
+ end
598
+ end
599
+
600
+ expect(schema.fields[:users]).to eq(
601
+ {
602
+ type: :dictionary,
603
+ description: nil,
604
+ value_type: {
605
+ type: :hash,
606
+ description: nil,
607
+ fields: {
608
+ name: {
609
+ type: :string,
610
+ description: nil
611
+ },
612
+ age: {
613
+ type: :integer,
614
+ description: nil
615
+ }
616
+ }
617
+ }
618
+ }
619
+ )
620
+ end
621
+
622
+ it "supports arrays as dictionary values" do
623
+ schema.dictionary(:categories) { array { string } }
624
+
625
+ expect(schema.fields[:categories]).to eq(
626
+ {
627
+ type: :dictionary,
628
+ description: nil,
629
+ value_type: {
630
+ type: :array,
631
+ description: nil,
632
+ element_type: {
633
+ type: :string,
634
+ description: nil
635
+ }
636
+ }
637
+ }
638
+ )
639
+ end
417
640
  end
418
641
 
419
642
  describe "primitive types" do
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: lluminary
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Doug Hughes
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-05-24 00:00:00.000000000 Z
11
+ date: 2025-06-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activemodel
@@ -241,6 +241,7 @@ files:
241
241
  - spec/examples/quote_task_spec.rb
242
242
  - spec/examples/sentiment_analysis_spec.rb
243
243
  - spec/examples/summarize_text_spec.rb
244
+ - spec/examples/text_emotion_analyzer_spec.rb
244
245
  - spec/lluminary/config_spec.rb
245
246
  - spec/lluminary/models/base_spec.rb
246
247
  - spec/lluminary/models/bedrock/amazon_nova_pro_v1_spec.rb