lutaml-model 0.7.1 → 0.7.3

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 (124) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -1
  3. data/.rubocop_todo.yml +49 -48
  4. data/Gemfile +4 -1
  5. data/README.adoc +791 -143
  6. data/RELEASE_NOTES.adoc +346 -0
  7. data/docs/custom_adapters.adoc +144 -0
  8. data/lib/lutaml/model/attribute.rb +17 -11
  9. data/lib/lutaml/model/config.rb +48 -42
  10. data/lib/lutaml/model/error/polymorphic_error.rb +7 -2
  11. data/lib/lutaml/model/format_registry.rb +41 -0
  12. data/lib/lutaml/model/hash/document.rb +11 -0
  13. data/lib/lutaml/model/hash/mapping.rb +19 -0
  14. data/lib/lutaml/model/hash/mapping_rule.rb +9 -0
  15. data/lib/lutaml/model/hash/standard_adapter.rb +17 -0
  16. data/lib/lutaml/model/hash/transform.rb +8 -0
  17. data/lib/lutaml/model/hash.rb +21 -0
  18. data/lib/lutaml/model/json/document.rb +11 -0
  19. data/lib/lutaml/model/json/mapping.rb +19 -0
  20. data/lib/lutaml/model/json/mapping_rule.rb +9 -0
  21. data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
  22. data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
  23. data/lib/lutaml/model/json/transform.rb +8 -0
  24. data/lib/lutaml/model/json.rb +21 -0
  25. data/lib/lutaml/model/key_value_document.rb +27 -0
  26. data/lib/lutaml/model/mapping/key_value_mapping.rb +8 -4
  27. data/lib/lutaml/model/mapping/mapping.rb +13 -0
  28. data/lib/lutaml/model/mapping/mapping_rule.rb +7 -6
  29. data/lib/lutaml/model/serialization_adapter.rb +22 -0
  30. data/lib/lutaml/model/serialize.rb +146 -521
  31. data/lib/lutaml/model/services/logger.rb +54 -0
  32. data/lib/lutaml/model/services/transformer.rb +48 -0
  33. data/lib/lutaml/model/services.rb +2 -0
  34. data/lib/lutaml/model/toml/document.rb +11 -0
  35. data/lib/lutaml/model/toml/mapping.rb +27 -0
  36. data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
  37. data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
  38. data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
  39. data/lib/lutaml/model/toml/transform.rb +8 -0
  40. data/lib/lutaml/model/toml.rb +30 -0
  41. data/lib/lutaml/model/transform/key_value_transform.rb +291 -0
  42. data/lib/lutaml/model/transform/xml_transform.rb +239 -0
  43. data/lib/lutaml/model/transform.rb +78 -0
  44. data/lib/lutaml/model/type/value.rb +6 -9
  45. data/lib/lutaml/model/uninitialized_class.rb +1 -1
  46. data/lib/lutaml/model/utils.rb +30 -0
  47. data/lib/lutaml/model/version.rb +1 -1
  48. data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
  49. data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
  50. data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
  51. data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +6 -7
  52. data/lib/lutaml/model/xml/element.rb +32 -0
  53. data/lib/lutaml/model/xml/mapping.rb +410 -0
  54. data/lib/lutaml/model/xml/mapping_rule.rb +141 -0
  55. data/lib/lutaml/model/xml/nokogiri_adapter.rb +232 -0
  56. data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
  57. data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +3 -1
  58. data/lib/lutaml/model/xml/oga_adapter.rb +171 -0
  59. data/lib/lutaml/model/xml/ox_adapter.rb +215 -0
  60. data/lib/lutaml/model/xml/transform.rb +8 -0
  61. data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
  62. data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +6 -3
  63. data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
  64. data/lib/lutaml/model/xml.rb +31 -0
  65. data/lib/lutaml/model/xml_adapter/element.rb +11 -25
  66. data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
  67. data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
  68. data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
  69. data/lib/lutaml/model/yaml/document.rb +10 -0
  70. data/lib/lutaml/model/yaml/mapping.rb +19 -0
  71. data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
  72. data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
  73. data/lib/lutaml/model/yaml/transform.rb +8 -0
  74. data/lib/lutaml/model/yaml.rb +21 -0
  75. data/lib/lutaml/model.rb +39 -4
  76. data/lutaml-model.gemspec +0 -4
  77. data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -4
  78. data/spec/lutaml/model/cdata_spec.rb +7 -7
  79. data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
  80. data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
  81. data/spec/lutaml/model/group_spec.rb +18 -7
  82. data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
  83. data/spec/lutaml/model/json_adapter_spec.rb +6 -6
  84. data/spec/lutaml/model/key_value_mapping_spec.rb +25 -1
  85. data/spec/lutaml/model/mixed_content_spec.rb +24 -24
  86. data/spec/lutaml/model/multiple_mapping_spec.rb +5 -5
  87. data/spec/lutaml/model/ordered_content_spec.rb +6 -6
  88. data/spec/lutaml/model/polymorphic_spec.rb +178 -0
  89. data/spec/lutaml/model/root_mappings_spec.rb +3 -3
  90. data/spec/lutaml/model/schema/xml_compiler_spec.rb +6 -6
  91. data/spec/lutaml/model/serializable_spec.rb +179 -103
  92. data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
  93. data/spec/lutaml/model/toml_spec.rb +51 -0
  94. data/spec/lutaml/model/transformation_spec.rb +72 -15
  95. data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
  96. data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
  97. data/spec/lutaml/model/xml/xml_element_spec.rb +1 -1
  98. data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
  99. data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
  100. data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
  101. data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
  102. data/spec/lutaml/model/xml_adapter_spec.rb +6 -6
  103. data/spec/lutaml/model/xml_mapping_rule_spec.rb +3 -3
  104. data/spec/lutaml/model/xml_mapping_spec.rb +26 -14
  105. data/spec/lutaml/model/xml_spec.rb +63 -0
  106. data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
  107. data/spec/spec_helper.rb +3 -3
  108. metadata +64 -59
  109. data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
  110. data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
  111. data/lib/lutaml/model/loggable.rb +0 -15
  112. data/lib/lutaml/model/mapping/json_mapping.rb +0 -17
  113. data/lib/lutaml/model/mapping/toml_mapping.rb +0 -25
  114. data/lib/lutaml/model/mapping/xml_mapping.rb +0 -389
  115. data/lib/lutaml/model/mapping/xml_mapping_rule.rb +0 -139
  116. data/lib/lutaml/model/mapping/yaml_mapping.rb +0 -17
  117. data/lib/lutaml/model/mapping.rb +0 -14
  118. data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
  119. data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
  120. data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
  121. data/lib/lutaml/model/toml_adapter.rb +0 -6
  122. data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
  123. data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
  124. data/lib/lutaml/model/yaml_adapter.rb +0 -8
@@ -0,0 +1,1226 @@
1
+ # The vObject Serialization Format for Lutaml::Model
2
+ #
3
+ # This module provides support for serializing and deserializing models to/from
4
+ # vObject format, a text-based data format used by standards like vCard and iCalendar.
5
+ #
6
+ # == Basic Usage
7
+ #
8
+ # To use vObject serialization in your models:
9
+ #
10
+ # class MyModel < Lutaml::Model::Serializable
11
+ # attribute :field1, :string
12
+ # attribute :field2, :string
13
+ #
14
+ # vobject do
15
+ # type :component # Declare as component (like VCARD, VCALENDAR)
16
+ # component_root "MYCOMP" # Root component name
17
+ #
18
+ # # Map vObject properties to model attributes
19
+ # map_property "PROP1", to: :field1
20
+ # map_property "PROP2", to: :field2
21
+ # end
22
+ # end
23
+ #
24
+ # == Creating Custom Components
25
+ #
26
+ # For non-standardized vObject components:
27
+ #
28
+ # class MyCustomComponent < Lutaml::Model::Serializable
29
+ # attribute :name, :string
30
+ # attribute :details, MyCustomDetails
31
+ #
32
+ # vobject do
33
+ # type :component
34
+ # component_root "X-CUSTOM" # Use X- prefix for custom components
35
+ #
36
+ # map_property "X-NAME", to: :name
37
+ # map_property "X-DETAILS", to: :details, type: :structured
38
+ # end
39
+ # end
40
+ #
41
+ # == Custom Properties
42
+ #
43
+ # Define custom properties with parameters:
44
+ #
45
+ # class MyCustomProperty < Lutaml::Model::Serializable
46
+ # attribute :value, :string
47
+ # attribute :param1, :string
48
+ # attribute :param2, :integer
49
+ #
50
+ # vobject do
51
+ # type :property
52
+ # property_root "X-CUSTOM-PROP"
53
+ #
54
+ # map_value to: :value
55
+ # map_property "PARAM1", to: :param1
56
+ # map_property "PARAM2", to: :param2
57
+ # end
58
+ # end
59
+ #
60
+ # == Value Types
61
+ #
62
+ # Support for different value types:
63
+ #
64
+ # - Text (default)
65
+ # - URI
66
+ # - Binary (Base64)
67
+ # - Boolean
68
+ # - Integer
69
+ # - Float
70
+ # - UTC-Offset
71
+ # - Language-Tag
72
+ # - IsoDateAndOrTime
73
+ #
74
+ # == Custom Value Types
75
+ #
76
+ # Create custom value types by extending VobjectValueType::Base:
77
+ #
78
+ # class MyCustomValue < VobjectValueType::Base
79
+ # def valid?
80
+ # # Add validation logic
81
+ # end
82
+ # end
83
+ #
84
+ # == Structured Values
85
+ #
86
+ # For properties with multiple fields:
87
+ #
88
+ # vobject do
89
+ # type :property
90
+ # property_root "X-STRUCTURED"
91
+ #
92
+ # map_field_set count: 3, item_type: :list
93
+ # map_field 0, to: :first_field
94
+ # map_field 1, to: :second_field
95
+ # map_field 2, to: :third_field
96
+ # end
97
+ #
98
+ # == Registration
99
+ #
100
+ # Register your custom format:
101
+ #
102
+ # Lutaml::Model::Config.register_format(
103
+ # :vobject,
104
+ # mapping_class: YourMapping,
105
+ # adapter_class: YourAdapter
106
+ # )
107
+ #
108
+ # == Component Types
109
+ #
110
+ # Four types of elements are supported:
111
+ # - :component - Container components like VCARD
112
+ # - :property - Properties with values and parameters
113
+ # - :property_parameter - Parameters for properties
114
+ # - :property_group - Grouping of related properties
115
+ require "spec_helper"
116
+
117
+ require_relative "../../../lib/lutaml/model/serialization_adapter"
118
+
119
+ module CustomVobjectAdapterSpec
120
+ class VobjectParser
121
+ def initialize(data)
122
+ @data = data
123
+ @current_object = nil
124
+ @objects = []
125
+ end
126
+
127
+ def parse
128
+ @data.each_line do |line|
129
+ line.chomp!
130
+ process_line(line)
131
+ end
132
+ @objects
133
+ end
134
+
135
+ private
136
+
137
+ def process_line(line)
138
+ case line
139
+ when /^BEGIN:/
140
+ @current_object = { name: line.split(":", 2).last, properties: {} }
141
+ when /^END:/
142
+ @objects << @current_object
143
+ @current_object = nil
144
+ else
145
+ add_property(line) if @current_object
146
+ end
147
+ end
148
+
149
+ def add_property(line)
150
+ prop_part, value = line.split(":", 2)
151
+ name, params = parse_property(prop_part)
152
+ @current_object[:properties][name] ||= []
153
+ @current_object[:properties][name] << { value: value, params: params }
154
+ end
155
+
156
+ def parse_property(prop_part)
157
+ parts = prop_part.split(";")
158
+ name = parts.shift.downcase
159
+ params = parts.each_with_object({}) do |part, hash|
160
+ k, v = part.split("=")
161
+ hash[k.downcase] = v
162
+ end
163
+ [name, params]
164
+ end
165
+ end
166
+
167
+ class VobjectDocument
168
+ attr_reader :objects
169
+
170
+ def initialize(objects = [])
171
+ @objects = objects
172
+ end
173
+
174
+ def self.parse(vobject_data, _options = {})
175
+ parser = VobjectParser.new(vobject_data)
176
+ new(parser.parse)
177
+ end
178
+
179
+ def to_vobject(*)
180
+ objects.map do |obj|
181
+ [
182
+ "BEGIN:#{obj[:name]}",
183
+ *obj[:properties].flat_map { |prop, entries| format_entries(prop.upcase, entries) },
184
+ "END:#{obj[:name]}",
185
+ ].join("\n")
186
+ end.join("\n\n")
187
+ end
188
+
189
+ def [](key)
190
+ @objects[key]
191
+ end
192
+
193
+ def []=(key, value)
194
+ @objects[key] = value
195
+ end
196
+
197
+ def to_h
198
+ @objects
199
+ end
200
+
201
+ def map(&block)
202
+ @objects.map(&block)
203
+ end
204
+
205
+ private
206
+
207
+ def format_entries(prop, entries)
208
+ entries.map do |entry|
209
+ params = (entry[:params] || {}).map { |k, v| "#{k}=#{v}" }.join(";")
210
+ prop_line = params.empty? ? prop : "#{prop};#{params}"
211
+ "#{prop_line}:#{entry[:value]}"
212
+ end
213
+ end
214
+ end
215
+
216
+ class VobjectAdapter < VobjectDocument
217
+ end
218
+
219
+ class VobjectMapping < Lutaml::Model::Mapping
220
+ attr_reader :mappings, :field_set
221
+
222
+ def initialize
223
+ super
224
+ @mappings = []
225
+ @structure_definitions = {}
226
+ @component_root = nil
227
+ @property_root = nil
228
+ @element_type = nil
229
+ end
230
+
231
+ def field_set?
232
+ @field_set
233
+ end
234
+
235
+ def component_root(name)
236
+ @component_root = name
237
+ end
238
+
239
+ def property_root(name)
240
+ @property_root = name
241
+ end
242
+
243
+ def type(element_type)
244
+ unless %i[component property property_parameter property_group].include?(element_type)
245
+ raise ArgumentError, "Invalid element type: #{element_type}"
246
+ end
247
+
248
+ @element_type = element_type
249
+ end
250
+
251
+ def map_value(to:, **options)
252
+ add_mapping(:value, to, type: :simple, **options)
253
+ end
254
+
255
+ def map_property(name, to:, type: :simple, **options)
256
+ add_mapping(name.downcase, to, type: type, **options)
257
+ end
258
+
259
+ def map_component(name, to:, **options)
260
+ add_mapping(name.downcase, to, type: :component, **options)
261
+ end
262
+
263
+ def map_field_set(count:, item_type:, item_options: {})
264
+ @field_set = {
265
+ count: count,
266
+ item_type: item_type,
267
+ options: item_options,
268
+ }
269
+ end
270
+
271
+ def map_field(index, to:, **options)
272
+ @mappings << FieldMappingRule.new(
273
+ index,
274
+ to: to,
275
+ type: @field_set[:item_type],
276
+ options: @field_set[:options].merge(options),
277
+ )
278
+ end
279
+
280
+ private
281
+
282
+ def add_mapping(name, to, **options)
283
+ @mappings << VobjectMappingRule.new(
284
+ name,
285
+ to: to,
286
+ type: options[:type],
287
+ structure: @structure_definitions[to],
288
+ options: options,
289
+ )
290
+ end
291
+ end
292
+
293
+ class VobjectMappingRule < Lutaml::Model::MappingRule
294
+ attr_reader :structure, :type, :options
295
+
296
+ def initialize(name, to:, type: :simple, structure: nil, options: {})
297
+ super(name, to: to)
298
+ @type = type
299
+ @structure = structure
300
+ @options = options
301
+ end
302
+
303
+ def deep_dup
304
+ self.class.new(
305
+ name.dup,
306
+ to: to.dup,
307
+ type: type.dup,
308
+ structure: structure&.dup,
309
+ options: options.dup,
310
+ )
311
+ end
312
+ end
313
+
314
+ class FieldMappingRule < VobjectMappingRule; end
315
+
316
+ class VobjectTransform < Lutaml::Model::Transform
317
+ def self.data_to_model(context, data, _format, _options = {})
318
+ new(context).data_to_model(data)
319
+ end
320
+
321
+ def self.model_to_data(context, model, _format, _options = {})
322
+ new(context).model_to_data(model)
323
+ end
324
+
325
+ def data_to_model(data)
326
+ mappings = context.mappings_for(:vobject)
327
+ instance = model_class.new
328
+
329
+ mappings.mappings.each do |mapping|
330
+ attribute = attributes[mapping.to]
331
+
332
+ value = if mapping.type == :component
333
+ data[0][:properties][mapping.name]
334
+ elsif mapping.type == :structured
335
+ handle_structured_value(data[0][:properties][mapping.name], attribute)
336
+ else
337
+ handle_property_value(data[0][:properties][mapping.name], attribute, mapping)
338
+ end
339
+
340
+ if value
341
+ instance.public_send(:"#{mapping.to}=", value)
342
+ end
343
+ end
344
+
345
+ instance
346
+ end
347
+
348
+ def model_to_data(model)
349
+ mappings = context.mappings_for(:vobject)
350
+ result = {}
351
+
352
+ mappings.mappings.each do |mapping|
353
+ attribute = attributes[mapping.to]
354
+ value = model.send(mapping.to)
355
+
356
+ if value
357
+ result[mapping.name] = format_property_value(value, attribute, mapping)
358
+ end
359
+ end
360
+
361
+ VobjectDocument.new([{ name: "VCARD", properties: result }])
362
+ end
363
+
364
+ private
365
+
366
+ def handle_property_value(data, attribute, mapping)
367
+ return nil unless data
368
+
369
+ if attribute.collection?
370
+ data.map { |entry| process_property_entry(entry, attribute, mapping) }
371
+ else
372
+ process_property_entry(data.first, attribute, mapping)
373
+ end
374
+ end
375
+
376
+ def process_property_entry(entry, attribute, mapping)
377
+ case mapping.name
378
+ when "tel"
379
+ create_vcard_tel(entry)
380
+ when "bday"
381
+ create_vcard_bday(entry)
382
+ else
383
+ attribute.type.from_vobject(entry[:value])
384
+ end
385
+ end
386
+
387
+ def create_vcard_tel(entry)
388
+ VcardTel.new(
389
+ value: entry[:value],
390
+ type: entry[:params]["TYPE"],
391
+ pref: entry[:params]["PREF"]&.to_i,
392
+ )
393
+ end
394
+
395
+ def create_vcard_bday(entry)
396
+ VcardBday.new(
397
+ value: VobjectPropertyValue.parse(entry[:value], "DATE-AND-OR-TIME"),
398
+ )
399
+ end
400
+
401
+ def handle_structured_value(data, attribute)
402
+ return nil unless data&.first&.dig(:value)
403
+
404
+ value_list = data.first[:value].split(";")
405
+
406
+ type_instance = attribute.type.new
407
+ attribute.type.mappings_for(:vobject).mappings.each do |mapping|
408
+ type_instance.public_send(:"#{mapping.to}=", value_list[mapping.name])
409
+ end
410
+
411
+ type_instance
412
+ end
413
+
414
+ def format_property_value(value, attribute, mapping)
415
+ if mapping.type == :structured
416
+ [{ value: format_structured_value(value, attribute) }]
417
+ elsif attribute.collection?
418
+ format_collection_value(value)
419
+ else
420
+ format_single_value(value)
421
+ end
422
+ end
423
+
424
+ def format_collection_value(value)
425
+ value.map do |v|
426
+ if v.is_a?(VcardTel)
427
+ format_tel_value(v)
428
+ else
429
+ { value: v.to_s }
430
+ end
431
+ end
432
+ end
433
+
434
+ def format_single_value(value)
435
+ if value.is_a?(VcardBday)
436
+ [{ value: value.value.to_s }]
437
+ elsif value.is_a?(VcardTel)
438
+ [format_tel_value(value)]
439
+ elsif value.respond_to?(:to_vobject)
440
+ [{ value: value.to_vobject }]
441
+ else
442
+ [{ value: value.to_s }]
443
+ end
444
+ end
445
+
446
+ def format_tel_value(tel)
447
+ params = { "TYPE" => tel.type, "PREF" => tel.pref }.compact
448
+ { value: tel.value, params: params }
449
+ end
450
+
451
+ def format_structured_value(value, attribute)
452
+ mappings = attribute.type.mappings_for(:vobject)
453
+ arr = Array.new(mappings.field_set[:count])
454
+
455
+ mappings.mappings.each do |mapping|
456
+ arr[mapping.name] = value.public_send(mapping.to)
457
+ end
458
+
459
+ arr.join(";")
460
+ end
461
+ end
462
+
463
+ module ElementType
464
+ COMPONENT = :component
465
+ PROPERTY = :property
466
+ PROPERTY_PARAMETER = :property_parameter
467
+ PROPERTY_GROUP = :property_group
468
+ end
469
+
470
+ module ValueType
471
+ PROPERTY_VALUE = :property_value
472
+ PARAMETER_VALUE = :parameter_value
473
+ end
474
+
475
+ module VobjectValueType
476
+ class Base < Lutaml::Model::Type::Value
477
+ attr_accessor :element_type
478
+
479
+ def initialize(value)
480
+ super
481
+ @element_type = :property_value
482
+ end
483
+
484
+ def as_property_value
485
+ @element_type = :property_value
486
+ self
487
+ end
488
+
489
+ def as_parameter_value
490
+ @element_type = :parameter_value
491
+ self
492
+ end
493
+
494
+ def valid?
495
+ true
496
+ end
497
+
498
+ def ==(other)
499
+ other.is_a?(self.class) && @value == other.value
500
+ end
501
+ end
502
+
503
+ class Text < Base
504
+ def valid?
505
+ @value.is_a?(String)
506
+ end
507
+ end
508
+
509
+ # URI as defined in Section 3 of [RFC3986]
510
+ class Uri < Base
511
+ def valid?
512
+ uri = URI.parse(@value)
513
+ !!(uri.scheme && uri.host)
514
+ rescue URI::InvalidURIError
515
+ false
516
+ end
517
+ end
518
+
519
+ class Binary < Base
520
+ def valid?
521
+ Base64.strict_encode64(Base64.strict_decode64(@value)) == @value
522
+ rescue ArgumentError
523
+ false
524
+ end
525
+ end
526
+
527
+ class IsoDateAndOrTime < Base
528
+ attr_reader :type
529
+
530
+ def initialize(value)
531
+ super
532
+ @type = detect_type(value)
533
+ end
534
+
535
+ def valid?
536
+ @type == detect_type(@value)
537
+ end
538
+
539
+ private
540
+
541
+ def detect_type(value)
542
+ case value
543
+ when /^\d{8}T\d{6}(Z|[+-]\d{4})?$/,
544
+ /^--\d{4}T\d{4}$/,
545
+ /^---\d{2}T\d{2}$/,
546
+ /^T\d{6}(Z|[+-]\d{4})?$/,
547
+ /^T\d{4}$/,
548
+ /^T\d{2}$/,
549
+ /^T-\d{4}$/,
550
+ /^T--\d{2}$/
551
+ :date_time
552
+ when /^\d{8}$/,
553
+ /^\d{6}$/,
554
+ /^\d{4}$/,
555
+ /^\d{4}-\d{2}$/,
556
+ /^\d{4}-\d{2}-\d{2}$/,
557
+ /^--\d{4}$/,
558
+ /^---\d{2}$/
559
+ :date
560
+ else
561
+ :text
562
+ end
563
+ end
564
+ end
565
+
566
+ class Boolean < Base
567
+ VALID_VALUES = %w[TRUE FALSE true false].freeze
568
+
569
+ def initialize(value)
570
+ super(value.to_s.upcase)
571
+ end
572
+
573
+ def valid?
574
+ VALID_VALUES.include?(@value)
575
+ end
576
+
577
+ def to_bool
578
+ @value.upcase == "TRUE"
579
+ end
580
+ end
581
+
582
+ class Integer < Base
583
+ def valid?
584
+ @value.to_s.match?(/^-?\d+$/)
585
+ end
586
+
587
+ def to_i
588
+ @value.to_i
589
+ end
590
+ end
591
+
592
+ class Float < Base
593
+ def valid?
594
+ @value.to_s.match?(/^-?\d+(\.\d+)?$/)
595
+ end
596
+
597
+ def to_f
598
+ @value.to_f
599
+ end
600
+ end
601
+
602
+ class UtcOffset < Base
603
+ def valid?
604
+ @value.match?(/^[+-]\d{4}$/)
605
+ end
606
+
607
+ def hours
608
+ @value[1..2].to_i * (@value[0] == "-" ? -1 : 1)
609
+ end
610
+
611
+ def minutes
612
+ @value[3..4].to_i * (@value[0] == "-" ? -1 : 1)
613
+ end
614
+ end
615
+
616
+ # Language-Tag as defined in [RFC5646]
617
+ class Language < Base
618
+ def valid?
619
+ # Basic validation for language tags
620
+ @value.match?(/^[a-zA-Z]{2,3}(-[a-zA-Z]{2,3})?$/)
621
+ end
622
+ end
623
+ end
624
+
625
+ Lutaml::Model::FormatRegistry.register(
626
+ :vobject,
627
+ mapping_class: VobjectMapping,
628
+ adapter_class: VobjectAdapter,
629
+ transformer: VobjectTransform,
630
+ )
631
+
632
+ class VcardBday < Lutaml::Model::Serializable
633
+ attribute :value, VobjectValueType::IsoDateAndOrTime
634
+
635
+ vobject do
636
+ type :property
637
+ property_root "BDAY"
638
+ map_value to: :value
639
+ end
640
+
641
+ def value_type
642
+ value.type
643
+ end
644
+ end
645
+
646
+ class VcardTel < Lutaml::Model::Serializable
647
+ attribute :value, :string
648
+ attribute :type, :string
649
+ attribute :pref, :integer
650
+
651
+ vobject do
652
+ type :property
653
+ property_root "TEL"
654
+
655
+ map_property "TYPE", to: :type
656
+ map_property "PREF", to: :pref
657
+ map_value to: :value
658
+ end
659
+ end
660
+
661
+ class VcardName < Lutaml::Model::Serializable
662
+ attribute :family, :string
663
+ attribute :given, :string
664
+ attribute :additional, :string
665
+ attribute :prefix, :string
666
+ attribute :suffix, :string
667
+
668
+ vobject do
669
+ type :property
670
+ property_root "N"
671
+
672
+ map_field_set(
673
+ count: 5,
674
+ item_type: :list,
675
+ item_options: { type: :text },
676
+ )
677
+ map_field 0, to: :family
678
+ map_field 1, to: :given
679
+ map_field 2, to: :additional
680
+ map_field 3, to: :prefix
681
+ map_field 4, to: :suffix
682
+ end
683
+ end
684
+
685
+ class Vcard < Lutaml::Model::Serializable
686
+ attribute :version, :string
687
+ attribute :fn, :string
688
+ attribute :n, VcardName
689
+ attribute :tel, VcardTel, collection: true
690
+ attribute :email, :string, collection: true
691
+ attribute :org, :string
692
+ attribute :bday, VcardBday # Using the enhanced VcardBday class
693
+
694
+ vobject do
695
+ type :component
696
+ component_root "VCARD"
697
+
698
+ map_property "VERSION", to: :version
699
+ map_property "FN", to: :fn
700
+ map_property "N", to: :n, type: :structured
701
+ map_property "TEL", to: :tel
702
+ map_property "EMAIL", to: :email
703
+ map_property "ORG", to: :org
704
+ map_property "BDAY", to: :bday
705
+ end
706
+ end
707
+
708
+ class VobjectPropertyValue
709
+ VALUE_TYPE_MAPPING = {
710
+ "URI" => VobjectValueType::Uri,
711
+ "BINARY" => VobjectValueType::Binary,
712
+ "BOOLEAN" => VobjectValueType::Boolean,
713
+ "INTEGER" => VobjectValueType::Integer,
714
+ "FLOAT" => VobjectValueType::Float,
715
+ "UTC-OFFSET" => VobjectValueType::UtcOffset,
716
+ "LANGUAGE-TAG" => VobjectValueType::Language,
717
+ "DATE" => VobjectValueType::IsoDateAndOrTime,
718
+ "DATE-TIME" => VobjectValueType::IsoDateAndOrTime,
719
+ "DATE-AND-OR-TIME" => VobjectValueType::IsoDateAndOrTime,
720
+ }.freeze
721
+
722
+ def self.parse(value_str, value_type = nil, element_type = :property_value)
723
+ unless %i[property_value parameter_value].include?(element_type)
724
+ raise ArgumentError, "Invalid element type: #{element_type}"
725
+ end
726
+
727
+ value_class = value_type ? VALUE_TYPE_MAPPING[value_type.upcase] : VobjectValueType::Text
728
+ value = value_class.new(value_str)
729
+ value.element_type = element_type
730
+ value
731
+ end
732
+ end
733
+
734
+ RSpec.describe VobjectValueType do
735
+ describe VobjectValueType::Text do
736
+ it "handles text values" do
737
+ text = described_class.new("Sample text")
738
+ expect(text.to_s).to eq("Sample text")
739
+ end
740
+ end
741
+
742
+ describe VobjectValueType::Uri do
743
+ it "validates valid URIs" do
744
+ uri = described_class.new("https://example.com")
745
+ expect(uri).to be_valid
746
+ end
747
+
748
+ it "invalidates malformed URIs" do
749
+ uri = described_class.new("not-a-uri")
750
+ expect(uri).not_to be_valid
751
+ end
752
+ end
753
+
754
+ describe VobjectValueType::Binary do
755
+ it "validates base64 encoded data" do
756
+ binary = described_class.new("SGVsbG8gV29ybGQ=") # "Hello World" in base64
757
+ expect(binary).to be_valid
758
+ end
759
+
760
+ it "invalidates malformed base64 data" do
761
+ binary = described_class.new("not-base64!")
762
+ expect(binary).not_to be_valid
763
+ end
764
+ end
765
+
766
+ describe VobjectValueType::Boolean do
767
+ it "handles true values" do
768
+ bool = described_class.new("TRUE")
769
+ expect(bool).to be_valid
770
+ expect(bool.to_bool).to be true
771
+ end
772
+
773
+ it "handles false values" do
774
+ bool = described_class.new("false")
775
+ expect(bool).to be_valid
776
+ expect(bool.to_bool).to be false
777
+ end
778
+
779
+ it "invalidates non-boolean values" do
780
+ bool = described_class.new("maybe")
781
+ expect(bool).not_to be_valid
782
+ end
783
+ end
784
+
785
+ describe VobjectValueType::Integer do
786
+ it "validates integer values" do
787
+ int = described_class.new("42")
788
+ expect(int).to be_valid
789
+ expect(int.to_i).to eq(42)
790
+ end
791
+
792
+ it "handles negative integers" do
793
+ int = described_class.new("-42")
794
+ expect(int).to be_valid
795
+ expect(int.to_i).to eq(-42)
796
+ end
797
+
798
+ it "truncates floats to integers" do
799
+ int = described_class.new("4.2")
800
+ expect(int.to_i).to eq(4)
801
+ end
802
+ end
803
+
804
+ describe VobjectValueType::Float do
805
+ it "validates float values" do
806
+ float = described_class.new("4.2")
807
+ expect(float).to be_valid
808
+ expect(float.to_f).to eq(4.2)
809
+ end
810
+
811
+ it "handles negative floats" do
812
+ float = described_class.new("-4.2")
813
+ expect(float).to be_valid
814
+ expect(float.to_f).to eq(-4.2)
815
+ end
816
+
817
+ it "handles integer-like floats" do
818
+ float = described_class.new("42")
819
+ expect(float).to be_valid
820
+ expect(float.to_f).to eq(42.0)
821
+ end
822
+ end
823
+
824
+ describe VobjectValueType::UtcOffset do
825
+ it "validates UTC offset format" do
826
+ offset = described_class.new("+0200")
827
+ expect(offset).to be_valid
828
+ expect(offset.hours).to eq(2)
829
+ expect(offset.minutes).to eq(0)
830
+ end
831
+
832
+ it "handles negative offsets" do
833
+ offset = described_class.new("-0500")
834
+ expect(offset).to be_valid
835
+ expect(offset.hours).to eq(-5)
836
+ expect(offset.minutes).to eq(0)
837
+ end
838
+
839
+ it "invalidates malformed offsets" do
840
+ offset = described_class.new("0200")
841
+ expect(offset).not_to be_valid
842
+ end
843
+ end
844
+
845
+ describe VobjectValueType::Language do
846
+ it "validates language tags" do
847
+ lang = described_class.new("en-US")
848
+ expect(lang).to be_valid
849
+ end
850
+
851
+ it "validates simple language codes" do
852
+ lang = described_class.new("en")
853
+ expect(lang).to be_valid
854
+ end
855
+
856
+ it "invalidates malformed language tags" do
857
+ lang = described_class.new("not-a-language")
858
+ expect(lang).not_to be_valid
859
+ end
860
+ end
861
+
862
+ # ... existing IsoDateAndOrTime tests ...
863
+ end
864
+
865
+ RSpec.describe VobjectValueType::IsoDateAndOrTime do
866
+ subject(:date_time) { described_class.new(value) }
867
+
868
+ context "when parsing date-time values" do
869
+ {
870
+ "19961022T140000" => :date_time,
871
+ "19961022T140000Z" => :date_time,
872
+ "--1022T1400" => :date_time,
873
+ "---22T14" => :date_time,
874
+ "T102200" => :date_time,
875
+ "T1022" => :date_time,
876
+ "T10" => :date_time,
877
+ "T-2200" => :date_time,
878
+ "T--00" => :date_time,
879
+ "T102200Z" => :date_time,
880
+ "T102200-0800" => :date_time,
881
+ }.each do |input, expected_type|
882
+ context "with #{input}" do
883
+ let(:value) { input }
884
+
885
+ it "detects correct type" do
886
+ expect(date_time.type).to eq(expected_type)
887
+ end
888
+
889
+ it "preserves original value" do
890
+ expect(date_time.to_s).to eq(input)
891
+ end
892
+ end
893
+ end
894
+ end
895
+
896
+ context "when parsing date values" do
897
+ {
898
+ "19850412" => :date,
899
+ "1985-04" => :date,
900
+ "1985" => :date,
901
+ "--0412" => :date,
902
+ "---12" => :date,
903
+ }.each do |input, expected_type|
904
+ context "with #{input}" do
905
+ let(:value) { input }
906
+
907
+ it "detects correct type" do
908
+ expect(date_time.type).to eq(expected_type)
909
+ end
910
+
911
+ it "preserves original value" do
912
+ expect(date_time.to_s).to eq(input)
913
+ end
914
+ end
915
+ end
916
+ end
917
+
918
+ context "when parsing text values" do
919
+ let(:value) { "Early April 1985" }
920
+
921
+ it "detects as text type" do
922
+ expect(date_time.type).to eq(:text)
923
+ end
924
+
925
+ it "preserves original value" do
926
+ expect(date_time.to_s).to eq(value)
927
+ end
928
+ end
929
+ end
930
+
931
+ RSpec.describe VobjectPropertyValue do
932
+ describe ".parse" do
933
+ it "creates Text value by default" do
934
+ value = described_class.parse("some text")
935
+ expect(value).to be_a(VobjectValueType::Text)
936
+ end
937
+
938
+ it "creates URI value" do
939
+ value = described_class.parse("https://example.com", "URI")
940
+ expect(value).to be_a(VobjectValueType::Uri)
941
+ expect(value).to be_valid
942
+ end
943
+
944
+ it "creates Binary value" do
945
+ value = described_class.parse("SGVsbG8=", "BINARY")
946
+ expect(value).to be_a(VobjectValueType::Binary)
947
+ expect(value).to be_valid
948
+ end
949
+
950
+ it "creates Boolean value" do
951
+ value = described_class.parse("TRUE", "BOOLEAN")
952
+ expect(value).to be_a(VobjectValueType::Boolean)
953
+ expect(value).to be_valid
954
+ expect(value.to_bool).to be true
955
+ end
956
+
957
+ it "creates Integer value" do
958
+ value = described_class.parse("42", "INTEGER")
959
+ expect(value).to be_a(VobjectValueType::Integer)
960
+ expect(value).to be_valid
961
+ expect(value.to_i).to eq(42)
962
+ end
963
+
964
+ it "creates Float value" do
965
+ value = described_class.parse("4.2", "FLOAT")
966
+ expect(value).to be_a(VobjectValueType::Float)
967
+ expect(value).to be_valid
968
+ expect(value.to_f).to eq(4.2)
969
+ end
970
+
971
+ it "creates UTC-OFFSET value" do
972
+ value = described_class.parse("+0200", "UTC-OFFSET")
973
+ expect(value).to be_a(VobjectValueType::UtcOffset)
974
+ expect(value).to be_valid
975
+ expect(value.hours).to eq(2)
976
+ end
977
+
978
+ it "creates LANGUAGE-TAG value" do
979
+ value = described_class.parse("en-US", "LANGUAGE-TAG")
980
+ expect(value).to be_a(VobjectValueType::Language)
981
+ expect(value).to be_valid
982
+ end
983
+
984
+ it "creates DATE-AND-OR-TIME value" do
985
+ value = described_class.parse("19961022T140000", "DATE-AND-OR-TIME")
986
+ expect(value).to be_a(VobjectValueType::IsoDateAndOrTime)
987
+ expect(value.type).to eq(:date_time)
988
+ end
989
+ end
990
+ end
991
+
992
+ RSpec.describe Vcard do
993
+ let(:full_vcard) do
994
+ described_class.new(
995
+ version: "4.0",
996
+ fn: "John Doe",
997
+ n: CustomVobjectAdapterSpec::VcardName.new(
998
+ family: "Doe",
999
+ given: "John",
1000
+ additional: "Middle",
1001
+ prefix: "Dr.",
1002
+ suffix: "PhD",
1003
+ ),
1004
+ tel: [
1005
+ CustomVobjectAdapterSpec::VcardTel.new(value: "tel:+1-555-555-5555"),
1006
+ CustomVobjectAdapterSpec::VcardTel.new(value: "tel:+1-555-555-1234"),
1007
+ ],
1008
+ email: ["john.doe@example.com", "j.doe@company.com"],
1009
+ org: "Example Corp",
1010
+ bday: CustomVobjectAdapterSpec::VcardBday.new(
1011
+ value: CustomVobjectAdapterSpec::VobjectPropertyValue.parse("1970-01-01", "DATE-AND-OR-TIME"),
1012
+ ),
1013
+ )
1014
+ end
1015
+
1016
+ let(:vobject_data) do
1017
+ <<~VCARD
1018
+ BEGIN:VCARD
1019
+ VERSION:4.0
1020
+ FN:John Doe
1021
+ N:Doe;John;Middle;Dr.;PhD
1022
+ TEL:tel:+1-555-555-5555
1023
+ TEL:tel:+1-555-555-1234
1024
+ EMAIL:john.doe@example.com
1025
+ EMAIL:j.doe@company.com
1026
+ ORG:Example Corp
1027
+ BDAY:1970-01-01
1028
+ END:VCARD
1029
+ VCARD
1030
+ end
1031
+
1032
+ describe "#to_vobject" do
1033
+ it "serializes all fields correctly" do
1034
+ output = full_vcard.to_vobject.gsub(/\s+/, " ").strip
1035
+ expected = vobject_data.gsub(/\s+/, " ").strip
1036
+
1037
+ expect(output).to eq(expected)
1038
+ end
1039
+ end
1040
+
1041
+ describe ".from_vobject" do
1042
+ let(:iso_date_time_vcard) do
1043
+ <<~VCARD
1044
+ BEGIN:VCARD
1045
+ VERSION:4.0
1046
+ FN:John Doe
1047
+ BDAY:19961022T140000Z
1048
+ END:VCARD
1049
+ VCARD
1050
+ end
1051
+
1052
+ let(:iso_date_vcard) do
1053
+ <<~VCARD
1054
+ BEGIN:VCARD
1055
+ VERSION:4.0
1056
+ FN:John Doe
1057
+ BDAY:19850412
1058
+ END:VCARD
1059
+ VCARD
1060
+ end
1061
+
1062
+ let(:text_date_vcard) do
1063
+ <<~VCARD
1064
+ BEGIN:VCARD
1065
+ VERSION:4.0
1066
+ FN:John Doe
1067
+ BDAY:Early April 1985
1068
+ END:VCARD
1069
+ VCARD
1070
+ end
1071
+
1072
+ it "parses all fields correctly" do
1073
+ card = described_class.from_vobject(vobject_data)
1074
+ expect(card).to eq(full_vcard)
1075
+ end
1076
+
1077
+ it "parses ISO date-time BDAY correctly" do
1078
+ card = described_class.from_vobject(iso_date_time_vcard)
1079
+ expect(card.bday.value.to_s).to eq("19961022T140000Z")
1080
+ expect(card.bday.value.type).to eq(:date_time)
1081
+ end
1082
+
1083
+ it "parses ISO date BDAY correctly" do
1084
+ card = described_class.from_vobject(iso_date_vcard)
1085
+ expect(card.bday.value.to_s).to eq("19850412")
1086
+ expect(card.bday.value.type).to eq(:date)
1087
+ end
1088
+
1089
+ it "parses text BDAY correctly" do
1090
+ card = described_class.from_vobject(text_date_vcard)
1091
+ expect(card.bday.value.to_s).to eq("Early April 1985")
1092
+ expect(card.bday.value.type).to eq(:text)
1093
+ end
1094
+ end
1095
+ end
1096
+
1097
+ RSpec.describe VobjectMapping do
1098
+ let(:mapping) { described_class.new }
1099
+
1100
+ describe "#type" do
1101
+ it "accepts valid element types" do
1102
+ expect { mapping.type(:component) }.not_to raise_error
1103
+ expect { mapping.type(:property) }.not_to raise_error
1104
+ expect { mapping.type(:property_parameter) }.not_to raise_error
1105
+ expect { mapping.type(:property_group) }.not_to raise_error
1106
+ end
1107
+
1108
+ it "rejects invalid element types" do
1109
+ expect { mapping.type(:invalid) }.to raise_error(ArgumentError)
1110
+ end
1111
+ end
1112
+
1113
+ describe "#component_root" do
1114
+ it "sets the component root" do
1115
+ mapping.component_root("VCARD")
1116
+ expect(mapping.instance_variable_get(:@component_root)).to eq("VCARD")
1117
+ end
1118
+ end
1119
+
1120
+ describe "#property_root" do
1121
+ it "sets the property root" do
1122
+ mapping.property_root("TEL")
1123
+ expect(mapping.instance_variable_get(:@property_root)).to eq("TEL")
1124
+ end
1125
+ end
1126
+
1127
+ describe "#map_field_set and #map_field" do
1128
+ it "maps fields correctly" do
1129
+ mapping.map_field_set(
1130
+ count: 3,
1131
+ item_type: :list,
1132
+ item_options: { type: :text },
1133
+ )
1134
+
1135
+ mapping.map_field(0, to: :first)
1136
+ mapping.map_field(1, to: :second)
1137
+ mapping.map_field(2, to: :third)
1138
+
1139
+ mappings = mapping.instance_variable_get(:@mappings)
1140
+ expect(mappings.size).to eq(3)
1141
+ expect(mappings[0].name).to eq(0)
1142
+ expect(mappings[0].to).to eq(:first)
1143
+ expect(mappings[0].type).to eq(:list)
1144
+ end
1145
+ end
1146
+
1147
+ describe "#map_property" do
1148
+ it "adds a property mapping with default type" do
1149
+ mapping.map_property("NAME", to: :name)
1150
+ mappings = mapping.instance_variable_get(:@mappings)
1151
+ expect(mappings.size).to eq(1)
1152
+ expect(mappings[0].name).to eq("name")
1153
+ expect(mappings[0].to).to eq(:name)
1154
+ expect(mappings[0].type).to eq(:simple)
1155
+ end
1156
+
1157
+ it "adds a property mapping with custom type" do
1158
+ mapping.map_property("NAME", to: :name, type: :structured)
1159
+ mappings = mapping.instance_variable_get(:@mappings)
1160
+ expect(mappings[0].type).to eq(:structured)
1161
+ end
1162
+ end
1163
+
1164
+ describe "#map_value" do
1165
+ it "adds a value mapping" do
1166
+ mapping.map_value(to: :value)
1167
+ mappings = mapping.instance_variable_get(:@mappings)
1168
+ expect(mappings.size).to eq(1)
1169
+ expect(mappings[0].name).to eq(:value)
1170
+ expect(mappings[0].to).to eq(:value)
1171
+ expect(mappings[0].type).to eq(:simple)
1172
+ end
1173
+ end
1174
+
1175
+ describe "#map_component" do
1176
+ it "adds a component mapping" do
1177
+ mapping.map_component("VCARD", to: :vcard)
1178
+ mappings = mapping.instance_variable_get(:@mappings)
1179
+ expect(mappings.size).to eq(1)
1180
+ expect(mappings[0].name).to eq("vcard")
1181
+ expect(mappings[0].to).to eq(:vcard)
1182
+ expect(mappings[0].type).to eq(:component)
1183
+ end
1184
+ end
1185
+ end
1186
+
1187
+ RSpec.describe VobjectMappingRule do
1188
+ let(:rule) { described_class.new("name", to: :name, type: :simple, structure: nil, options: {}) }
1189
+
1190
+ describe "#initialize" do
1191
+ it "sets all attributes correctly" do
1192
+ expect(rule.name).to eq("name")
1193
+ expect(rule.to).to eq(:name)
1194
+ expect(rule.type).to eq(:simple)
1195
+ expect(rule.structure).to be_nil
1196
+ expect(rule.options).to eq({})
1197
+ end
1198
+ end
1199
+
1200
+ describe "#deep_dup" do
1201
+ it "creates a deep copy of the rule" do
1202
+ duped = rule.deep_dup
1203
+ expect(duped).to be_a(described_class)
1204
+ expect(duped.name).to eq(rule.name)
1205
+ expect(duped.to).to eq(rule.to)
1206
+ expect(duped.type).to eq(rule.type)
1207
+ expect(duped.structure).to eq(rule.structure)
1208
+ expect(duped.options).to eq(rule.options)
1209
+ expect(duped).not_to equal(rule)
1210
+ end
1211
+ end
1212
+ end
1213
+
1214
+ RSpec.describe FieldMappingRule do
1215
+ let(:rule) { described_class.new(0, to: :field, type: :list, options: {}) }
1216
+
1217
+ describe "#initialize" do
1218
+ it "sets all attributes correctly" do
1219
+ expect(rule.name).to eq(0)
1220
+ expect(rule.to).to eq(:field)
1221
+ expect(rule.type).to eq(:list)
1222
+ expect(rule.options).to eq({})
1223
+ end
1224
+ end
1225
+ end
1226
+ end