lutaml-model 0.3.24 → 0.3.25

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.
Files changed (80) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +35 -16
  3. data/README.adoc +274 -28
  4. data/lib/lutaml/model/attribute.rb +18 -8
  5. data/lib/lutaml/model/error/type_error.rb +9 -0
  6. data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
  7. data/lib/lutaml/model/error/validation_error.rb +0 -1
  8. data/lib/lutaml/model/error.rb +2 -0
  9. data/lib/lutaml/model/serialize.rb +6 -1
  10. data/lib/lutaml/model/type/boolean.rb +38 -0
  11. data/lib/lutaml/model/type/date.rb +35 -0
  12. data/lib/lutaml/model/type/date_time.rb +32 -4
  13. data/lib/lutaml/model/type/decimal.rb +42 -0
  14. data/lib/lutaml/model/type/float.rb +37 -0
  15. data/lib/lutaml/model/type/hash.rb +62 -0
  16. data/lib/lutaml/model/type/integer.rb +41 -0
  17. data/lib/lutaml/model/type/string.rb +49 -0
  18. data/lib/lutaml/model/type/time.rb +49 -0
  19. data/lib/lutaml/model/type/time_without_date.rb +37 -5
  20. data/lib/lutaml/model/type/value.rb +52 -0
  21. data/lib/lutaml/model/type.rb +50 -114
  22. data/lib/lutaml/model/version.rb +1 -1
  23. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +5 -2
  24. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +2 -1
  25. data/lib/lutaml/model/xml_adapter/xml_document.rb +0 -2
  26. data/lutaml-model.gemspec +1 -1
  27. data/spec/address_spec.rb +170 -0
  28. data/spec/fixtures/address.rb +33 -0
  29. data/spec/fixtures/person.rb +73 -0
  30. data/spec/fixtures/sample_model.rb +40 -0
  31. data/spec/fixtures/vase.rb +38 -0
  32. data/spec/fixtures/xml/special_char.xml +13 -0
  33. data/spec/lutaml/model/attribute_spec.rb +112 -0
  34. data/spec/lutaml/model/collection_spec.rb +299 -0
  35. data/spec/lutaml/model/comparable_model_spec.rb +106 -0
  36. data/spec/lutaml/model/custom_model_spec.rb +410 -0
  37. data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
  38. data/spec/lutaml/model/defaults_spec.rb +221 -0
  39. data/spec/lutaml/model/delegation_spec.rb +340 -0
  40. data/spec/lutaml/model/inheritance_spec.rb +92 -0
  41. data/spec/lutaml/model/json_adapter_spec.rb +37 -0
  42. data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
  43. data/spec/lutaml/model/map_content_spec.rb +118 -0
  44. data/spec/lutaml/model/mixed_content_spec.rb +625 -0
  45. data/spec/lutaml/model/namespace_spec.rb +57 -0
  46. data/spec/lutaml/model/ordered_content_spec.rb +83 -0
  47. data/spec/lutaml/model/render_nil_spec.rb +138 -0
  48. data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
  49. data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
  50. data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
  51. data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
  52. data/spec/lutaml/model/serializable_spec.rb +297 -0
  53. data/spec/lutaml/model/serializable_validation_spec.rb +85 -0
  54. data/spec/lutaml/model/simple_model_spec.rb +314 -0
  55. data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
  56. data/spec/lutaml/model/type/boolean_spec.rb +54 -0
  57. data/spec/lutaml/model/type/date_spec.rb +118 -0
  58. data/spec/lutaml/model/type/date_time_spec.rb +127 -0
  59. data/spec/lutaml/model/type/decimal_spec.rb +125 -0
  60. data/spec/lutaml/model/type/float_spec.rb +191 -0
  61. data/spec/lutaml/model/type/hash_spec.rb +63 -0
  62. data/spec/lutaml/model/type/integer_spec.rb +145 -0
  63. data/spec/lutaml/model/type/string_spec.rb +150 -0
  64. data/spec/lutaml/model/type/time_spec.rb +142 -0
  65. data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
  66. data/spec/lutaml/model/type_spec.rb +276 -0
  67. data/spec/lutaml/model/utils_spec.rb +79 -0
  68. data/spec/lutaml/model/validation_spec.rb +83 -0
  69. data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
  70. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
  71. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
  72. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
  73. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
  74. data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
  75. data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
  76. data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
  77. data/spec/lutaml/model_spec.rb +1 -0
  78. data/spec/person_spec.rb +161 -0
  79. data/spec/spec_helper.rb +33 -0
  80. metadata +66 -2
@@ -0,0 +1,63 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::Hash do
4
+ describe ".cast" do
5
+ it "returns nil for nil input" do
6
+ expect(described_class.cast(nil)).to be_nil
7
+ end
8
+
9
+ it "returns text content from hash with only text key" do
10
+ hash = { "text" => "content" }
11
+ expect(described_class.cast(hash)).to eq "content"
12
+ end
13
+
14
+ it "normalizes MappingHash to regular Hash" do
15
+ mapping_hash = Lutaml::Model::MappingHash.new
16
+ mapping_hash["key"] = "value"
17
+ expect(described_class.cast(mapping_hash)).to eq({ "key" => "value" })
18
+ end
19
+
20
+ it "filters out text keys from nested hashes" do
21
+ hash = {
22
+ "key1" => {
23
+ "text" => "content1",
24
+ "other" => "value1",
25
+ },
26
+ "key2" => {
27
+ "text" => "content2",
28
+ "other" => "value2",
29
+ },
30
+ }
31
+ expected = {
32
+ "key1" => { "other" => "value1" },
33
+ "key2" => { "other" => "value2" },
34
+ }
35
+ expect(described_class.cast(hash)).to eq expected
36
+ end
37
+
38
+ it "preserves non-hash values" do
39
+ input = {
40
+ "string" => "value",
41
+ "number" => 42,
42
+ "array" => [1, 2, 3],
43
+ }
44
+ expect(described_class.cast(input)).to eq input
45
+ end
46
+ end
47
+
48
+ describe ".serialize" do
49
+ it "returns nil for nil input" do
50
+ expect(described_class.serialize(nil)).to be_nil
51
+ end
52
+
53
+ it "converts hash to hash" do
54
+ hash = { "key" => "value" }
55
+ expect(described_class.serialize(hash)).to eq hash
56
+ end
57
+
58
+ it "converts arbitrary object responding to to_h" do
59
+ obj = double(to_h: { "key" => "value" })
60
+ expect(described_class.serialize(obj)).to eq({ "key" => "value" })
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,145 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::Integer do
4
+ describe ".cast" do
5
+ subject(:cast) { described_class.cast(value) }
6
+
7
+ context "with nil value" do
8
+ let(:value) { nil }
9
+
10
+ it { is_expected.to be_nil }
11
+ end
12
+
13
+ context "with integer" do
14
+ let(:value) { 123 }
15
+
16
+ it { is_expected.to eq(123) }
17
+ end
18
+
19
+ context "with integer string" do
20
+ let(:value) { "123" }
21
+
22
+ it { is_expected.to eq(123) }
23
+ end
24
+
25
+ context "with negative integer string" do
26
+ let(:value) { "-123" }
27
+
28
+ it { is_expected.to eq(-123) }
29
+ end
30
+
31
+ context "with float" do
32
+ let(:value) { 123.45 }
33
+
34
+ it { is_expected.to eq(123) }
35
+ end
36
+
37
+ context "with float string" do
38
+ let(:value) { "123.45" }
39
+
40
+ it { is_expected.to eq(123) }
41
+ end
42
+
43
+ context "with exponential notation" do
44
+ let(:value) { "1.23e2" }
45
+
46
+ it { is_expected.to eq(123) }
47
+ end
48
+
49
+ context "with negative exponential notation" do
50
+ let(:value) { "-1.23e2" }
51
+
52
+ it { is_expected.to eq(-123) }
53
+ end
54
+
55
+ context "with string containing a leading zero represents octal" do
56
+ let(:value) { "0123" }
57
+
58
+ it { is_expected.to eq(83) }
59
+ end
60
+
61
+ context "with plus sign" do
62
+ let(:value) { "+123" }
63
+
64
+ it { is_expected.to eq(123) }
65
+ end
66
+
67
+ context "with whitespace" do
68
+ let(:value) { " 123 " }
69
+
70
+ it { is_expected.to eq(123) }
71
+ end
72
+
73
+ context "with boolean true" do
74
+ let(:value) { true }
75
+
76
+ it { is_expected.to eq(1) }
77
+ end
78
+
79
+ context "with boolean false" do
80
+ let(:value) { false }
81
+
82
+ it { is_expected.to eq(0) }
83
+ end
84
+
85
+ context "with invalid string" do
86
+ let(:value) { "not an integer" }
87
+
88
+ it { is_expected.to be_nil }
89
+ end
90
+
91
+ context "with very large integer" do
92
+ let(:max_value) { ((2**((0.size * 8) - 2)) - 1) }
93
+ let(:value) { max_value.to_s }
94
+
95
+ xit { is_expected.to eq(max_value) }
96
+ end
97
+
98
+ context "with very small integer" do
99
+ let(:min_value) { -(2**((0.size * 8) - 2)) }
100
+ let(:value) { min_value.to_s }
101
+
102
+ it { is_expected.to eq(min_value) }
103
+ end
104
+ end
105
+
106
+ describe ".serialize" do
107
+ subject(:serialize) { described_class.serialize(value) }
108
+
109
+ context "with nil value" do
110
+ let(:value) { nil }
111
+
112
+ it { is_expected.to be_nil }
113
+ end
114
+
115
+ context "with positive integer" do
116
+ let(:value) { 123 }
117
+
118
+ it { is_expected.to eq(123) }
119
+ end
120
+
121
+ context "with negative integer" do
122
+ let(:value) { -123 }
123
+
124
+ it { is_expected.to eq(-123) }
125
+ end
126
+
127
+ context "with zero" do
128
+ let(:value) { 0 }
129
+
130
+ it { is_expected.to eq(0) }
131
+ end
132
+
133
+ context "with maximum integer" do
134
+ let(:value) { 9223372036854775807 }
135
+
136
+ it { is_expected.to eq(9223372036854775807) }
137
+ end
138
+
139
+ context "with minimum integer" do
140
+ let(:value) { -9223372036854775808 }
141
+
142
+ it { is_expected.to eq(-9223372036854775808) }
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,150 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::String do
4
+ describe ".cast" do
5
+ subject(:cast) { described_class.cast(value) }
6
+
7
+ context "with nil value" do
8
+ let(:value) { nil }
9
+
10
+ it { is_expected.to be_nil }
11
+ end
12
+
13
+ context "with string value" do
14
+ let(:value) { "test" }
15
+
16
+ it { is_expected.to eq("test") }
17
+ end
18
+
19
+ context "with symbol" do
20
+ let(:value) { :symbol }
21
+
22
+ it { is_expected.to eq("symbol") }
23
+ end
24
+
25
+ context "with integer" do
26
+ let(:value) { 123 }
27
+
28
+ it { is_expected.to eq("123") }
29
+ end
30
+
31
+ context "with float" do
32
+ let(:value) { 123.45 }
33
+
34
+ it { is_expected.to eq("123.45") }
35
+ end
36
+
37
+ context "with true" do
38
+ let(:value) { true }
39
+
40
+ it { is_expected.to eq("true") }
41
+ end
42
+
43
+ context "with false" do
44
+ let(:value) { false }
45
+
46
+ it { is_expected.to eq("false") }
47
+ end
48
+
49
+ context "with Date object" do
50
+ let(:value) { Date.new(2024, 1, 1) }
51
+
52
+ it { is_expected.to eq("2024-01-01") }
53
+ end
54
+
55
+ context "with Time object" do
56
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0, "+00:00") }
57
+
58
+ it { is_expected.to match(/\A2024-01-01 12:00:00/) }
59
+ end
60
+
61
+ context "with array" do
62
+ let(:value) { [1, 2, 3] }
63
+
64
+ it { is_expected.to eq("[1, 2, 3]") }
65
+ end
66
+
67
+ context "with hash" do
68
+ let(:value) { { a: 1, b: 2 } }
69
+ let(:expected_value) do
70
+ if RUBY_VERSION < "3.4.0"
71
+ "{:a=>1, :b=>2}"
72
+ else
73
+ "{a: 1, b: 2}"
74
+ end
75
+ end
76
+
77
+ it { is_expected.to eq(expected_value) }
78
+ end
79
+
80
+ context "with object responding to to_s" do
81
+ let(:value) do
82
+ Class.new do
83
+ def to_s
84
+ "custom string"
85
+ end
86
+ end.new
87
+ end
88
+
89
+ it { is_expected.to eq("custom string") }
90
+ end
91
+
92
+ context "with empty string" do
93
+ let(:value) { "" }
94
+
95
+ it { is_expected.to eq("") }
96
+ end
97
+
98
+ context "with whitespace string" do
99
+ let(:value) { " \t\n " }
100
+
101
+ it { is_expected.to eq(" \t\n ") }
102
+ end
103
+ end
104
+
105
+ describe ".serialize" do
106
+ subject(:serialize) { described_class.serialize(value) }
107
+
108
+ context "with nil value" do
109
+ let(:value) { nil }
110
+
111
+ it { is_expected.to be_nil }
112
+ end
113
+
114
+ context "with string value" do
115
+ let(:value) { "test" }
116
+
117
+ it { is_expected.to eq("test") }
118
+ end
119
+
120
+ context "with empty string" do
121
+ let(:value) { "" }
122
+
123
+ it { is_expected.to eq("") }
124
+ end
125
+
126
+ context "with whitespace string" do
127
+ let(:value) { " \t\n " }
128
+
129
+ it { is_expected.to eq(" \t\n ") }
130
+ end
131
+
132
+ context "with string containing special characters" do
133
+ let(:value) { "test\u0000test" }
134
+
135
+ it { is_expected.to eq("test\u0000test") }
136
+ end
137
+
138
+ context "with unicode characters" do
139
+ let(:value) { "こんにちは" }
140
+
141
+ it { is_expected.to eq("こんにちは") }
142
+ end
143
+
144
+ context "with emoji" do
145
+ let(:value) { "Hello 👋" }
146
+
147
+ it { is_expected.to eq("Hello 👋") }
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,142 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::Time do
4
+ describe ".cast" do
5
+ subject(:cast) { described_class.cast(value) }
6
+
7
+ context "with nil value" do
8
+ let(:value) { nil }
9
+
10
+ it { is_expected.to be_nil }
11
+ end
12
+
13
+ context "with valid Time string" do
14
+ let(:value) { "2024-01-01T12:00:00Z" }
15
+
16
+ it "parses with UTC offset" do
17
+ expect(cast.utc_offset).to eq(0)
18
+ expect(cast.strftime("%:z")).to eq("+00:00")
19
+ end
20
+ end
21
+
22
+ context "with positive timezone offset" do
23
+ let(:value) { "2024-01-01T12:00:00+08:00" }
24
+
25
+ it "retains the positive offset" do
26
+ expect(cast.utc_offset).to eq(8 * 3600)
27
+ expect(cast.strftime("%:z")).to eq("+08:00")
28
+ end
29
+ end
30
+
31
+ context "with negative timezone offset" do
32
+ let(:value) { "2024-01-01T12:00:00-05:00" }
33
+
34
+ it "retains the negative offset" do
35
+ expect(cast.utc_offset).to eq(-5 * 3600)
36
+ expect(cast.strftime("%:z")).to eq("-05:00")
37
+ end
38
+ end
39
+
40
+ context "with fractional offset" do
41
+ let(:value) { "2024-01-01T12:00:00+05:30" }
42
+
43
+ it "retains the fractional offset" do
44
+ expect(cast.utc_offset).to eq((5 * 3600) + (30 * 60))
45
+ expect(cast.strftime("%:z")).to eq("+05:30")
46
+ end
47
+ end
48
+
49
+ context "with DateTime object" do
50
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0, "+08:00") }
51
+
52
+ it "preserves the offset" do
53
+ expect(cast.utc_offset).to eq(8 * 3600)
54
+ expect(cast.strftime("%:z")).to eq("+08:00")
55
+ end
56
+ end
57
+
58
+ context "with Time object" do
59
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0, "+08:00") }
60
+
61
+ it "preserves the offset" do
62
+ expect(cast.utc_offset).to eq(8 * 3600)
63
+ expect(cast.strftime("%:z")).to eq("+08:00")
64
+ end
65
+ end
66
+
67
+ context "with invalid Time string" do
68
+ let(:value) { "not a time" }
69
+
70
+ it { is_expected.to be_nil }
71
+ end
72
+
73
+ context "with microsecond precision" do
74
+ let(:value) { "2024-01-01T12:00:00.123456+08:00" }
75
+
76
+ it "retains microsecond precision" do
77
+ expect(cast.usec).to eq(123456)
78
+ expect(cast.strftime("%:z")).to eq("+08:00")
79
+ end
80
+ end
81
+ end
82
+
83
+ describe ".serialize" do
84
+ subject(:serialize) { described_class.serialize(value) }
85
+
86
+ context "with nil value" do
87
+ let(:value) { nil }
88
+
89
+ it { is_expected.to be_nil }
90
+ end
91
+
92
+ context "with UTC Time" do
93
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0, "+00:00") }
94
+
95
+ it "serializes with +00:00 offset" do
96
+ expect(serialize).to eq("2024-01-01T12:00:00+00:00")
97
+ end
98
+ end
99
+
100
+ context "with positive offset" do
101
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0, "+08:00") }
102
+
103
+ it "retains positive offset in serialized form" do
104
+ expect(serialize).to eq("2024-01-01T12:00:00+08:00")
105
+ end
106
+ end
107
+
108
+ context "with negative offset" do
109
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0, "-05:00") }
110
+
111
+ it "retains negative offset in serialized form" do
112
+ expect(serialize).to eq("2024-01-01T12:00:00-05:00")
113
+ end
114
+ end
115
+
116
+ context "with fractional offset" do
117
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0, "+05:30") }
118
+
119
+ it "retains fractional offset in serialized form" do
120
+ expect(serialize).to eq("2024-01-01T12:00:00+05:30")
121
+ end
122
+ end
123
+
124
+ context "with fractional seconds" do
125
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0.5, "+08:00") }
126
+
127
+ xit "retains both fractional seconds and offset" do
128
+ expect(serialize).to eq("2024-01-01T12:00:00.500+08:00")
129
+ end
130
+ end
131
+
132
+ context "with microsecond precision" do
133
+ let(:value) { Time.at(Time.new(2024, 1, 1, 12).to_i, 123456, :usec) }
134
+
135
+ before { value.localtime("+08:00") }
136
+
137
+ xit "retains microsecond precision and offset" do
138
+ expect(serialize).to eq("2024-01-01T12:00:00.123456+08:00")
139
+ end
140
+ end
141
+ end
142
+ end
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::TimeWithoutDate do
4
+ describe ".cast" do
5
+ subject(:cast) { described_class.cast(value) }
6
+
7
+ context "with nil value" do
8
+ let(:value) { nil }
9
+
10
+ it { is_expected.to be_nil }
11
+ end
12
+
13
+ context "with time string" do
14
+ let(:value) { "13:45:30" }
15
+
16
+ it "parses time correctly" do
17
+ expect(cast.hour).to eq(13)
18
+ expect(cast.min).to eq(45)
19
+ expect(cast.sec).to eq(30)
20
+ end
21
+ end
22
+
23
+ context "with time string with milliseconds" do
24
+ let(:value) { "13:45:30.500" }
25
+
26
+ it "parses time with milliseconds" do
27
+ expect(cast.hour).to eq(13)
28
+ expect(cast.min).to eq(45)
29
+ expect(cast.sec).to eq(30)
30
+ expect(cast.nsec).to eq(500_000_000)
31
+ end
32
+ end
33
+
34
+ context "with Time object" do
35
+ let(:value) { Time.new(2024, 1, 1, 13, 45, 30) }
36
+
37
+ it "extracts time components" do
38
+ expect(cast.hour).to eq(13)
39
+ expect(cast.min).to eq(45)
40
+ expect(cast.sec).to eq(30)
41
+ end
42
+ end
43
+
44
+ context "with DateTime object" do
45
+ let(:value) { DateTime.new(2024, 1, 1, 13, 45, 30) }
46
+
47
+ it "extracts time components" do
48
+ expect(cast.hour).to eq(13)
49
+ expect(cast.min).to eq(45)
50
+ expect(cast.sec).to eq(30)
51
+ end
52
+ end
53
+
54
+ context "with invalid time string" do
55
+ let(:value) { "not a time" }
56
+
57
+ it { is_expected.to be_nil }
58
+ end
59
+
60
+ context "with invalid hours" do
61
+ let(:value) { "24:00:00" }
62
+
63
+ xit { is_expected.to be_nil }
64
+ end
65
+
66
+ context "with invalid minutes" do
67
+ let(:value) { "12:60:00" }
68
+
69
+ it { is_expected.to be_nil }
70
+ end
71
+
72
+ context "with invalid seconds" do
73
+ let(:value) { "12:00:61" }
74
+
75
+ it { is_expected.to be_nil }
76
+ end
77
+
78
+ context "with microsecond precision" do
79
+ let(:value) { "13:45:30.123456" }
80
+
81
+ it "retains microsecond precision" do
82
+ expect(cast.hour).to eq(13)
83
+ expect(cast.min).to eq(45)
84
+ expect(cast.sec).to eq(30)
85
+ expect(cast.usec).to eq(123456)
86
+ end
87
+ end
88
+ end
89
+
90
+ describe ".serialize" do
91
+ subject(:serialize) { described_class.serialize(value) }
92
+
93
+ context "with nil value" do
94
+ let(:value) { nil }
95
+
96
+ it { is_expected.to be_nil }
97
+ end
98
+
99
+ context "with Time object" do
100
+ let(:value) { Time.new(2024, 1, 1, 13, 45, 30) }
101
+
102
+ it { is_expected.to eq("13:45:30") }
103
+ end
104
+
105
+ context "with single-digit values" do
106
+ let(:value) { Time.new(2024, 1, 1, 9, 5, 3) }
107
+
108
+ it "zero-pads values" do
109
+ expect(serialize).to eq("09:05:03")
110
+ end
111
+ end
112
+
113
+ context "with double-digit values" do
114
+ let(:value) { Time.new(2024, 1, 1, 13, 45, 30) }
115
+
116
+ it { is_expected.to eq("13:45:30") }
117
+ end
118
+
119
+ context "with zero values" do
120
+ let(:value) { Time.new(2024, 1, 1, 0, 0, 0) }
121
+
122
+ it { is_expected.to eq("00:00:00") }
123
+ end
124
+ end
125
+ end