lutaml-model 0.3.24 → 0.3.26

Sign up to get free protection for your applications and to get access to all the features.
Files changed (88) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop_todo.yml +36 -17
  3. data/README.adoc +396 -30
  4. data/lib/lutaml/model/attribute.rb +52 -27
  5. data/lib/lutaml/model/error/pattern_not_matched_error.rb +17 -0
  6. data/lib/lutaml/model/error/type_error.rb +9 -0
  7. data/lib/lutaml/model/error/unknown_type_error.rb +9 -0
  8. data/lib/lutaml/model/error/validation_error.rb +0 -1
  9. data/lib/lutaml/model/error.rb +3 -0
  10. data/lib/lutaml/model/mapping_hash.rb +8 -0
  11. data/lib/lutaml/model/serialize.rb +10 -5
  12. data/lib/lutaml/model/type/boolean.rb +38 -0
  13. data/lib/lutaml/model/type/date.rb +35 -0
  14. data/lib/lutaml/model/type/date_time.rb +32 -4
  15. data/lib/lutaml/model/type/decimal.rb +42 -0
  16. data/lib/lutaml/model/type/float.rb +37 -0
  17. data/lib/lutaml/model/type/hash.rb +62 -0
  18. data/lib/lutaml/model/type/integer.rb +41 -0
  19. data/lib/lutaml/model/type/string.rb +49 -0
  20. data/lib/lutaml/model/type/time.rb +49 -0
  21. data/lib/lutaml/model/type/time_without_date.rb +37 -5
  22. data/lib/lutaml/model/type/value.rb +52 -0
  23. data/lib/lutaml/model/type.rb +50 -114
  24. data/lib/lutaml/model/validation.rb +2 -1
  25. data/lib/lutaml/model/version.rb +1 -1
  26. data/lib/lutaml/model/xml_adapter/builder/nokogiri.rb +12 -3
  27. data/lib/lutaml/model/xml_adapter/builder/ox.rb +7 -1
  28. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +3 -1
  29. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +12 -8
  30. data/lib/lutaml/model/xml_adapter/xml_document.rb +6 -8
  31. data/lib/lutaml/model/xml_mapping.rb +6 -2
  32. data/lib/lutaml/model/xml_mapping_rule.rb +7 -1
  33. data/lutaml-model.gemspec +1 -1
  34. data/spec/address_spec.rb +170 -0
  35. data/spec/fixtures/address.rb +33 -0
  36. data/spec/fixtures/person.rb +73 -0
  37. data/spec/fixtures/sample_model.rb +40 -0
  38. data/spec/fixtures/vase.rb +38 -0
  39. data/spec/fixtures/xml/special_char.xml +13 -0
  40. data/spec/lutaml/model/attribute_spec.rb +139 -0
  41. data/spec/lutaml/model/cdata_spec.rb +520 -0
  42. data/spec/lutaml/model/collection_spec.rb +299 -0
  43. data/spec/lutaml/model/comparable_model_spec.rb +106 -0
  44. data/spec/lutaml/model/custom_model_spec.rb +410 -0
  45. data/spec/lutaml/model/custom_serialization_spec.rb +170 -0
  46. data/spec/lutaml/model/defaults_spec.rb +221 -0
  47. data/spec/lutaml/model/delegation_spec.rb +340 -0
  48. data/spec/lutaml/model/inheritance_spec.rb +92 -0
  49. data/spec/lutaml/model/json_adapter_spec.rb +37 -0
  50. data/spec/lutaml/model/key_value_mapping_spec.rb +86 -0
  51. data/spec/lutaml/model/map_content_spec.rb +118 -0
  52. data/spec/lutaml/model/mixed_content_spec.rb +625 -0
  53. data/spec/lutaml/model/namespace_spec.rb +57 -0
  54. data/spec/lutaml/model/ordered_content_spec.rb +83 -0
  55. data/spec/lutaml/model/render_nil_spec.rb +138 -0
  56. data/spec/lutaml/model/schema/json_schema_spec.rb +79 -0
  57. data/spec/lutaml/model/schema/relaxng_schema_spec.rb +60 -0
  58. data/spec/lutaml/model/schema/xsd_schema_spec.rb +55 -0
  59. data/spec/lutaml/model/schema/yaml_schema_spec.rb +47 -0
  60. data/spec/lutaml/model/serializable_spec.rb +297 -0
  61. data/spec/lutaml/model/serializable_validation_spec.rb +90 -0
  62. data/spec/lutaml/model/simple_model_spec.rb +314 -0
  63. data/spec/lutaml/model/toml_adapter_spec.rb +39 -0
  64. data/spec/lutaml/model/type/boolean_spec.rb +54 -0
  65. data/spec/lutaml/model/type/date_spec.rb +118 -0
  66. data/spec/lutaml/model/type/date_time_spec.rb +127 -0
  67. data/spec/lutaml/model/type/decimal_spec.rb +125 -0
  68. data/spec/lutaml/model/type/float_spec.rb +191 -0
  69. data/spec/lutaml/model/type/hash_spec.rb +63 -0
  70. data/spec/lutaml/model/type/integer_spec.rb +145 -0
  71. data/spec/lutaml/model/type/string_spec.rb +150 -0
  72. data/spec/lutaml/model/type/time_spec.rb +142 -0
  73. data/spec/lutaml/model/type/time_without_date_spec.rb +125 -0
  74. data/spec/lutaml/model/type_spec.rb +276 -0
  75. data/spec/lutaml/model/utils_spec.rb +79 -0
  76. data/spec/lutaml/model/validation_spec.rb +83 -0
  77. data/spec/lutaml/model/with_child_mapping_spec.rb +174 -0
  78. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +56 -0
  79. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +56 -0
  80. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +61 -0
  81. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +251 -0
  82. data/spec/lutaml/model/xml_adapter_spec.rb +178 -0
  83. data/spec/lutaml/model/xml_mapping_spec.rb +863 -0
  84. data/spec/lutaml/model/yaml_adapter_spec.rb +30 -0
  85. data/spec/lutaml/model_spec.rb +1 -0
  86. data/spec/person_spec.rb +161 -0
  87. data/spec/spec_helper.rb +33 -0
  88. metadata +68 -2
@@ -0,0 +1,39 @@
1
+ require "spec_helper"
2
+ require "lutaml/model/toml_adapter/toml_rb_adapter"
3
+ require "lutaml/model/toml_adapter/tomlib_adapter"
4
+ require_relative "../../fixtures/sample_model"
5
+
6
+ RSpec.shared_examples "a TOML adapter" do |adapter_class|
7
+ let(:attributes) { { name: "John Doe", age: 30 } }
8
+ let(:model) { SampleModel.new(attributes) }
9
+
10
+ let(:expected_toml) do
11
+ if adapter_class == Lutaml::Model::TomlAdapter::TomlRbAdapter
12
+ TomlRB.dump(attributes)
13
+ elsif adapter_class == Lutaml::Model::TomlAdapter::TomlibAdapter
14
+ Tomlib.dump(attributes)
15
+ end
16
+ end
17
+
18
+ it "serializes to TOML" do
19
+ toml = adapter_class.new(attributes).to_toml
20
+
21
+ expect(toml).to eq(expected_toml)
22
+ end
23
+
24
+ it "deserializes from TOML" do
25
+ doc = adapter_class.parse(expected_toml)
26
+ new_model = SampleModel.new(doc.to_h)
27
+
28
+ expect(new_model.name).to eq("John Doe")
29
+ expect(new_model.age).to eq(30)
30
+ end
31
+ end
32
+
33
+ RSpec.describe Lutaml::Model::TomlAdapter::TomlRbAdapter do
34
+ it_behaves_like "a TOML adapter", described_class
35
+ end
36
+
37
+ RSpec.describe Lutaml::Model::TomlAdapter::TomlibAdapter do
38
+ it_behaves_like "a TOML adapter", described_class
39
+ end
@@ -0,0 +1,54 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::Boolean do
4
+ describe ".cast" do
5
+ let(:truthy_values) { [true, "true", "t", "yes", "y", "1"] }
6
+ let(:falsey_values) { [false, "false", "f", "no", "n", "0"] }
7
+
8
+ it "returns nil for nil input" do
9
+ expect(described_class.cast(nil)).to be_nil
10
+ end
11
+
12
+ context "with truthy values" do
13
+ it "casts to true" do
14
+ truthy_values.each do |value|
15
+ expect(described_class.cast(value)).to be true
16
+ end
17
+ end
18
+ end
19
+
20
+ context "with falsey values" do
21
+ it "casts to false" do
22
+ falsey_values.each do |value|
23
+ expect(described_class.cast(value)).to be false
24
+ end
25
+ end
26
+ end
27
+
28
+ context "with other values" do
29
+ it "returns the original value" do
30
+ value = "other"
31
+ expect(described_class.cast(value)).to eq value
32
+ end
33
+ end
34
+ end
35
+
36
+ describe ".serialize" do
37
+ it "returns nil for nil input" do
38
+ expect(described_class.serialize(nil)).to be_nil
39
+ end
40
+
41
+ it "returns true for truthy input" do
42
+ expect(described_class.serialize(true)).to be true
43
+ end
44
+
45
+ it "returns false for falsey input" do
46
+ expect(described_class.serialize(false)).to be false
47
+ end
48
+
49
+ it "preserves input boolean values" do
50
+ expect(described_class.serialize(false)).to be false
51
+ expect(described_class.serialize(true)).to be true
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,118 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::Date 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 Date string" do
14
+ let(:value) { "2024-01-01" }
15
+
16
+ it { is_expected.to eq(Date.new(2024, 1, 1)) }
17
+ end
18
+
19
+ context "with Date object" do
20
+ let(:value) { Date.new(2024, 1, 1) }
21
+
22
+ it { is_expected.to eq(value) }
23
+ end
24
+
25
+ context "with DateTime object" do
26
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0) }
27
+
28
+ it { is_expected.to eq(Date.new(2024, 1, 1)) }
29
+ end
30
+
31
+ context "with Time object" do
32
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0) }
33
+
34
+ it { is_expected.to eq(Date.new(2024, 1, 1)) }
35
+ end
36
+
37
+ context "with invalid date string" do
38
+ let(:value) { "not a date" }
39
+
40
+ it { is_expected.to be_nil }
41
+ end
42
+
43
+ context "with invalid month" do
44
+ let(:value) { "2024-13-01" }
45
+
46
+ it { is_expected.to be_nil }
47
+ end
48
+
49
+ context "with invalid day" do
50
+ let(:value) { "2024-04-31" }
51
+
52
+ it { is_expected.to be_nil }
53
+ end
54
+
55
+ context "with different date formats" do
56
+ it "parses ISO 8601" do
57
+ expect(described_class.cast("2024-01-01")).to eq(Date.new(2024, 1, 1))
58
+ end
59
+
60
+ it "parses RFC 3339" do
61
+ expect(described_class.cast("2024-01-01T12:00:00Z")).to eq(Date.new(
62
+ 2024, 1, 1
63
+ ))
64
+ end
65
+
66
+ it "parses common formats" do
67
+ expect(described_class.cast("01/01/2024")).to eq(Date.new(2024, 1, 1))
68
+ expect(described_class.cast("Jan 1, 2024")).to eq(Date.new(2024, 1, 1))
69
+ end
70
+ end
71
+
72
+ context "with leap year dates" do
73
+ it "handles February 29 in leap years" do
74
+ expect(described_class.cast("2024-02-29")).to eq(Date.new(2024, 2, 29))
75
+ end
76
+
77
+ it "rejects February 29 in non-leap years" do
78
+ expect(described_class.cast("2023-02-29")).to be_nil
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 Date object" do
93
+ let(:value) { Date.new(2024, 1, 1) }
94
+
95
+ it { is_expected.to eq("2024-01-01") }
96
+ end
97
+
98
+ context "with single-digit month and day" do
99
+ let(:value) { Date.new(2024, 1, 1) }
100
+
101
+ it "zero-pads month and day" do
102
+ expect(serialize).to eq("2024-01-01")
103
+ end
104
+ end
105
+
106
+ context "with double-digit month and day" do
107
+ let(:value) { Date.new(2024, 12, 31) }
108
+
109
+ it { is_expected.to eq("2024-12-31") }
110
+ end
111
+
112
+ context "with leap year date" do
113
+ let(:value) { Date.new(2024, 2, 29) }
114
+
115
+ it { is_expected.to eq("2024-02-29") }
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,127 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::DateTime 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 DateTime string" do
14
+ let(:value) { "2024-01-01T12:00:00Z" }
15
+
16
+ it "parses with UTC offset" do
17
+ expect(cast.offset).to eq(0)
18
+ expect(cast.zone).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.offset).to eq(Rational(8, 24))
27
+ expect(cast.zone).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.offset).to eq(Rational(-5, 24))
36
+ expect(cast.zone).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.offset).to eq(Rational(5.5, 24))
45
+ expect(cast.zone).to eq("+05:30")
46
+ end
47
+ end
48
+
49
+ context "with Time object with offset" do
50
+ let(:value) { Time.new(2024, 1, 1, 12, 0, 0, "+08:00") }
51
+
52
+ it "preserves the offset from Time object" do
53
+ expect(cast.offset).to eq(Rational(8, 24))
54
+ expect(cast.zone).to eq("+08:00")
55
+ end
56
+ end
57
+
58
+ context "with DateTime object with offset" do
59
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0, "+08:00") }
60
+
61
+ it "preserves the offset from DateTime object" do
62
+ expect(cast.offset).to eq(Rational(8, 24))
63
+ expect(cast.zone).to eq("+08:00")
64
+ end
65
+ end
66
+ end
67
+
68
+ describe ".serialize" do
69
+ subject(:serialize) { described_class.serialize(value) }
70
+
71
+ context "with nil value" do
72
+ let(:value) { nil }
73
+
74
+ it { is_expected.to be_nil }
75
+ end
76
+
77
+ context "with UTC DateTime" do
78
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0) }
79
+
80
+ it "serializes with +00:00 offset" do
81
+ expect(serialize).to eq("2024-01-01T12:00:00+00:00")
82
+ end
83
+ end
84
+
85
+ context "with positive offset" do
86
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0, "+08:00") }
87
+
88
+ it "retains positive offset in serialized form" do
89
+ expect(serialize).to eq("2024-01-01T12:00:00+08:00")
90
+ end
91
+ end
92
+
93
+ context "with negative offset" do
94
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0, "-05:00") }
95
+
96
+ it "retains negative offset in serialized form" do
97
+ expect(serialize).to eq("2024-01-01T12:00:00-05:00")
98
+ end
99
+ end
100
+
101
+ context "with fractional offset" do
102
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0, "+05:30") }
103
+
104
+ it "retains fractional offset in serialized form" do
105
+ expect(serialize).to eq("2024-01-01T12:00:00+05:30")
106
+ end
107
+ end
108
+
109
+ context "with fractional seconds" do
110
+ let(:value) { DateTime.new(2024, 1, 1, 12, 0, 0.5, "+08:00") }
111
+
112
+ xit "retains both fractional seconds and offset" do
113
+ expect(serialize).to eq("2024-01-01T12:00:00.500+08:00")
114
+ end
115
+ end
116
+
117
+ context "with microsecond precision" do
118
+ let(:value) do
119
+ DateTime.new(2024, 1, 1, 12, 0, Rational(123456, 1000000), "+08:00")
120
+ end
121
+
122
+ xit "retains microsecond precision and offset" do
123
+ expect(serialize).to eq("2024-01-01T12:00:00.123456+08:00")
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,125 @@
1
+ require "spec_helper"
2
+ require "bigdecimal"
3
+
4
+ RSpec.describe Lutaml::Model::Type do
5
+ describe "Decimal type" do
6
+ let(:decimal_class) { Lutaml::Model::Type::Decimal }
7
+
8
+ context "when bigdecimal is loaded" do
9
+ before do
10
+ # Ensure BigDecimal is loaded
11
+ require "bigdecimal"
12
+ end
13
+
14
+ it "serializes into Lutaml::Model::Type::Decimal" do
15
+ value = BigDecimal("123.45")
16
+ serialized = decimal_class.serialize(value)
17
+ expect(serialized).to eq("123.45")
18
+ end
19
+
20
+ it "deserializes into Ruby BigDecimal" do
21
+ value = "123.45"
22
+ deserialized = decimal_class.cast(value)
23
+ expect(deserialized).to be_a(BigDecimal)
24
+ expect(deserialized).to eq(BigDecimal("123.45"))
25
+ end
26
+ end
27
+
28
+ context "when bigdecimal is not loaded" do
29
+ before do
30
+ hide_const("BigDecimal")
31
+ end
32
+
33
+ it "raises TypeNotEnabledError when serializing" do
34
+ expect do
35
+ decimal_class.serialize(123.45)
36
+ end.to raise_error(Lutaml::Model::TypeNotEnabledError, /Decimal/)
37
+ end
38
+
39
+ it "raises TypeNotEnabledError when deserializing" do
40
+ expect do
41
+ decimal_class.cast("123.45")
42
+ end.to raise_error(Lutaml::Model::TypeNotEnabledError, /Decimal/)
43
+ end
44
+ end
45
+ end
46
+
47
+ describe ".cast" do
48
+ context "with BigDecimal available" do
49
+ before do
50
+ # Ensure BigDecimal is loaded
51
+ require "bigdecimal"
52
+ end
53
+
54
+ let(:decimal_class) { Lutaml::Model::Type::Decimal }
55
+
56
+ it "returns nil for nil input" do
57
+ expect(decimal_class.cast(nil)).to be_nil
58
+ end
59
+
60
+ it "casts numeric string to BigDecimal" do
61
+ expect(decimal_class.cast("123.45")).to eq BigDecimal("123.45")
62
+ end
63
+
64
+ it "casts integer to BigDecimal" do
65
+ expect(decimal_class.cast(123)).to eq BigDecimal("123")
66
+ end
67
+
68
+ it "casts float to BigDecimal" do
69
+ expect(decimal_class.cast(123.45)).to eq BigDecimal("123.45")
70
+ end
71
+
72
+ it "casts BigDecimal to BigDecimal" do
73
+ value = BigDecimal("123.45")
74
+ expect(decimal_class.cast(value)).to eq value
75
+ end
76
+ end
77
+
78
+ context "with BigDecimal not available", :skip_before do
79
+ before do
80
+ hide_const("BigDecimal")
81
+ end
82
+
83
+ it "raises TypeNotEnabledError" do
84
+ expect { Lutaml::Model::Type::Decimal.cast("123.45") }.to raise_error(
85
+ Lutaml::Model::TypeNotEnabledError,
86
+ /Decimal/,
87
+ )
88
+ end
89
+ end
90
+ end
91
+
92
+ describe ".serialize" do
93
+ context "with BigDecimal available" do
94
+ let(:decimal_class) { Lutaml::Model::Type::Decimal }
95
+
96
+ before do
97
+ # Ensure BigDecimal is loaded
98
+ require "bigdecimal"
99
+ end
100
+
101
+ it "returns nil for nil input" do
102
+ expect(decimal_class.serialize(nil)).to be_nil
103
+ end
104
+
105
+ it "serializes BigDecimal to string" do
106
+ expect(decimal_class.serialize(BigDecimal("123.45"))).to eq "123.45"
107
+ end
108
+ end
109
+
110
+ context "with BigDecimal not available", :skip_before do
111
+ before do
112
+ hide_const("BigDecimal")
113
+ end
114
+
115
+ it "raises TypeNotEnabledError" do
116
+ expect do
117
+ Lutaml::Model::Type::Decimal.serialize("123.45")
118
+ end.to raise_error(
119
+ Lutaml::Model::TypeNotEnabledError,
120
+ /Decimal/,
121
+ )
122
+ end
123
+ end
124
+ end
125
+ end
@@ -0,0 +1,191 @@
1
+ require "spec_helper"
2
+
3
+ RSpec.describe Lutaml::Model::Type::Float 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 float" do
14
+ let(:value) { 123.45 }
15
+
16
+ it { is_expected.to eq(123.45) }
17
+ end
18
+
19
+ context "with integer" do
20
+ let(:value) { 123 }
21
+
22
+ it { is_expected.to eq(123.0) }
23
+ end
24
+
25
+ context "with float string" do
26
+ let(:value) { "123.45" }
27
+
28
+ it { is_expected.to eq(123.45) }
29
+ end
30
+
31
+ context "with integer string" do
32
+ let(:value) { "123" }
33
+
34
+ it { is_expected.to eq(123.0) }
35
+ end
36
+
37
+ context "with negative float" do
38
+ let(:value) { -123.45 }
39
+
40
+ it { is_expected.to eq(-123.45) }
41
+ end
42
+
43
+ context "with negative float string" do
44
+ let(:value) { "-123.45" }
45
+
46
+ it { is_expected.to eq(-123.45) }
47
+ end
48
+
49
+ context "with exponential notation" do
50
+ let(:value) { "1.2345e2" }
51
+
52
+ it { is_expected.to eq(123.45) }
53
+ end
54
+
55
+ context "with negative exponential notation" do
56
+ let(:value) { "-1.2345e2" }
57
+
58
+ it { is_expected.to eq(-123.45) }
59
+ end
60
+
61
+ context "with very small exponential notation" do
62
+ let(:value) { "1.2345e-2" }
63
+
64
+ it { is_expected.to eq(0.012345) }
65
+ end
66
+
67
+ context "with string containing leading zeros" do
68
+ let(:value) { "000123.45" }
69
+
70
+ it { is_expected.to eq(123.45) }
71
+ end
72
+
73
+ context "with trailing zeros" do
74
+ let(:value) { "123.4500" }
75
+
76
+ it { is_expected.to eq(123.45) }
77
+ end
78
+
79
+ context "with plus sign" do
80
+ let(:value) { "+123.45" }
81
+
82
+ it { is_expected.to eq(123.45) }
83
+ end
84
+
85
+ context "with whitespace" do
86
+ let(:value) { " 123.45 " }
87
+
88
+ it { is_expected.to eq(123.45) }
89
+ end
90
+
91
+ context "with invalid string" do
92
+ let(:value) { "not a float" }
93
+
94
+ it { is_expected.to eq(0.0) }
95
+ end
96
+
97
+ context "with special float values" do
98
+ it "handles infinity" do
99
+ expect(described_class.cast(Float::INFINITY)).to eq(Float::INFINITY)
100
+ expect(described_class.cast(-Float::INFINITY)).to eq(-Float::INFINITY)
101
+ end
102
+
103
+ it "handles NaN" do
104
+ expect(described_class.cast(Float::NAN)).to be_nan
105
+ end
106
+ end
107
+
108
+ context "with very large numbers" do
109
+ let(:value) { "1.23456789e100" }
110
+
111
+ it { is_expected.to eq(1.23456789e100) }
112
+ end
113
+
114
+ context "with very small numbers" do
115
+ let(:value) { "1.23456789e-100" }
116
+
117
+ it { is_expected.to eq(1.23456789e-100) }
118
+ end
119
+
120
+ context "with precision edge cases" do
121
+ it "handles floating point precision" do
122
+ expect(described_class.cast(0.1 + 0.2)).to be_within(0.0000001).of(0.3)
123
+ end
124
+ end
125
+ end
126
+
127
+ describe ".serialize" do
128
+ subject(:serialize) { described_class.serialize(value) }
129
+
130
+ context "with nil value" do
131
+ let(:value) { nil }
132
+
133
+ it { is_expected.to be_nil }
134
+ end
135
+
136
+ context "with positive float" do
137
+ let(:value) { 123.45 }
138
+
139
+ it { is_expected.to eq(123.45) }
140
+ end
141
+
142
+ context "with negative float" do
143
+ let(:value) { -123.45 }
144
+
145
+ it { is_expected.to eq(-123.45) }
146
+ end
147
+
148
+ context "with zero" do
149
+ let(:value) { 0.0 }
150
+
151
+ it { is_expected.to eq(0.0) }
152
+ end
153
+
154
+ context "with integer-like float" do
155
+ let(:value) { 123.0 }
156
+
157
+ it { is_expected.to eq(123.0) }
158
+ end
159
+
160
+ context "with very small number" do
161
+ let(:value) { 0.000000123 }
162
+
163
+ it { is_expected.to eq(0.000000123) }
164
+ end
165
+
166
+ context "with very large number" do
167
+ let(:value) { 1.23e100 }
168
+
169
+ it { is_expected.to eq(1.23e100) }
170
+ end
171
+
172
+ context "with special float values" do
173
+ it "serializes infinity" do
174
+ expect(described_class.serialize(Float::INFINITY)).to eq(Float::INFINITY)
175
+ expect(described_class.serialize(-Float::INFINITY)).to eq(-Float::INFINITY)
176
+ end
177
+
178
+ it "serializes NaN" do
179
+ expect(described_class.serialize(Float::NAN)).to be_nan
180
+ end
181
+ end
182
+
183
+ context "with maximum precision" do
184
+ let(:value) { 123.456789123456789 }
185
+
186
+ it "maintains precision" do
187
+ expect(serialize).to eq(123.456789123456789)
188
+ end
189
+ end
190
+ end
191
+ end