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.
- checksums.yaml +4 -4
- data/.rubocop.yml +1 -1
- data/.rubocop_todo.yml +49 -48
- data/Gemfile +4 -1
- data/README.adoc +791 -143
- data/RELEASE_NOTES.adoc +346 -0
- data/docs/custom_adapters.adoc +144 -0
- data/lib/lutaml/model/attribute.rb +17 -11
- data/lib/lutaml/model/config.rb +48 -42
- data/lib/lutaml/model/error/polymorphic_error.rb +7 -2
- data/lib/lutaml/model/format_registry.rb +41 -0
- data/lib/lutaml/model/hash/document.rb +11 -0
- data/lib/lutaml/model/hash/mapping.rb +19 -0
- data/lib/lutaml/model/hash/mapping_rule.rb +9 -0
- data/lib/lutaml/model/hash/standard_adapter.rb +17 -0
- data/lib/lutaml/model/hash/transform.rb +8 -0
- data/lib/lutaml/model/hash.rb +21 -0
- data/lib/lutaml/model/json/document.rb +11 -0
- data/lib/lutaml/model/json/mapping.rb +19 -0
- data/lib/lutaml/model/json/mapping_rule.rb +9 -0
- data/lib/lutaml/model/{json_adapter → json}/multi_json_adapter.rb +4 -5
- data/lib/lutaml/model/{json_adapter/standard_json_adapter.rb → json/standard_adapter.rb} +5 -3
- data/lib/lutaml/model/json/transform.rb +8 -0
- data/lib/lutaml/model/json.rb +21 -0
- data/lib/lutaml/model/key_value_document.rb +27 -0
- data/lib/lutaml/model/mapping/key_value_mapping.rb +8 -4
- data/lib/lutaml/model/mapping/mapping.rb +13 -0
- data/lib/lutaml/model/mapping/mapping_rule.rb +7 -6
- data/lib/lutaml/model/serialization_adapter.rb +22 -0
- data/lib/lutaml/model/serialize.rb +146 -521
- data/lib/lutaml/model/services/logger.rb +54 -0
- data/lib/lutaml/model/services/transformer.rb +48 -0
- data/lib/lutaml/model/services.rb +2 -0
- data/lib/lutaml/model/toml/document.rb +11 -0
- data/lib/lutaml/model/toml/mapping.rb +27 -0
- data/lib/lutaml/model/toml/mapping_rule.rb +9 -0
- data/lib/lutaml/model/{toml_adapter → toml}/toml_rb_adapter.rb +3 -3
- data/lib/lutaml/model/toml/tomlib_adapter.rb +19 -0
- data/lib/lutaml/model/toml/transform.rb +8 -0
- data/lib/lutaml/model/toml.rb +30 -0
- data/lib/lutaml/model/transform/key_value_transform.rb +291 -0
- data/lib/lutaml/model/transform/xml_transform.rb +239 -0
- data/lib/lutaml/model/transform.rb +78 -0
- data/lib/lutaml/model/type/value.rb +6 -9
- data/lib/lutaml/model/uninitialized_class.rb +1 -1
- data/lib/lutaml/model/utils.rb +30 -0
- data/lib/lutaml/model/version.rb +1 -1
- data/lib/lutaml/model/{xml_adapter → xml}/builder/nokogiri.rb +2 -2
- data/lib/lutaml/model/{xml_adapter → xml}/builder/oga.rb +10 -10
- data/lib/lutaml/model/{xml_adapter → xml}/builder/ox.rb +1 -1
- data/lib/lutaml/model/{xml_adapter/xml_document.rb → xml/document.rb} +6 -7
- data/lib/lutaml/model/xml/element.rb +32 -0
- data/lib/lutaml/model/xml/mapping.rb +410 -0
- data/lib/lutaml/model/xml/mapping_rule.rb +141 -0
- data/lib/lutaml/model/xml/nokogiri_adapter.rb +232 -0
- data/lib/lutaml/model/{xml_adapter → xml}/oga/document.rb +1 -1
- data/lib/lutaml/model/{xml_adapter → xml}/oga/element.rb +3 -1
- data/lib/lutaml/model/xml/oga_adapter.rb +171 -0
- data/lib/lutaml/model/xml/ox_adapter.rb +215 -0
- data/lib/lutaml/model/xml/transform.rb +8 -0
- data/lib/lutaml/model/{xml_adapter → xml}/xml_attribute.rb +1 -1
- data/lib/lutaml/model/{xml_adapter → xml}/xml_element.rb +6 -3
- data/lib/lutaml/model/{xml_adapter → xml}/xml_namespace.rb +1 -1
- data/lib/lutaml/model/xml.rb +31 -0
- data/lib/lutaml/model/xml_adapter/element.rb +11 -25
- data/lib/lutaml/model/xml_adapter/nokogiri_adapter.rb +6 -223
- data/lib/lutaml/model/xml_adapter/oga_adapter.rb +13 -163
- data/lib/lutaml/model/xml_adapter/ox_adapter.rb +10 -207
- data/lib/lutaml/model/yaml/document.rb +10 -0
- data/lib/lutaml/model/yaml/mapping.rb +19 -0
- data/lib/lutaml/model/yaml/mapping_rule.rb +9 -0
- data/lib/lutaml/model/{yaml_adapter/standard_yaml_adapter.rb → yaml/standard_adapter.rb} +4 -3
- data/lib/lutaml/model/yaml/transform.rb +8 -0
- data/lib/lutaml/model/yaml.rb +21 -0
- data/lib/lutaml/model.rb +39 -4
- data/lutaml-model.gemspec +0 -4
- data/spec/benchmarks/xml_parsing_benchmark_spec.rb +4 -4
- data/spec/lutaml/model/cdata_spec.rb +7 -7
- data/spec/lutaml/model/custom_bibtex_adapter_spec.rb +598 -0
- data/spec/lutaml/model/custom_vobject_adapter_spec.rb +1226 -0
- data/spec/lutaml/model/group_spec.rb +18 -7
- data/spec/lutaml/model/hash/adapter_spec.rb +255 -0
- data/spec/lutaml/model/json_adapter_spec.rb +6 -6
- data/spec/lutaml/model/key_value_mapping_spec.rb +25 -1
- data/spec/lutaml/model/mixed_content_spec.rb +24 -24
- data/spec/lutaml/model/multiple_mapping_spec.rb +5 -5
- data/spec/lutaml/model/ordered_content_spec.rb +6 -6
- data/spec/lutaml/model/polymorphic_spec.rb +178 -0
- data/spec/lutaml/model/root_mappings_spec.rb +3 -3
- data/spec/lutaml/model/schema/xml_compiler_spec.rb +6 -6
- data/spec/lutaml/model/serializable_spec.rb +179 -103
- data/spec/lutaml/model/toml_adapter_spec.rb +6 -6
- data/spec/lutaml/model/toml_spec.rb +51 -0
- data/spec/lutaml/model/transformation_spec.rb +72 -15
- data/spec/lutaml/model/uninitialized_class_spec.rb +96 -0
- data/spec/lutaml/model/xml/namespace_spec.rb +57 -0
- data/spec/lutaml/model/xml/xml_element_spec.rb +1 -1
- data/spec/lutaml/model/xml_adapter/nokogiri_adapter_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/oga_adapter_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/ox_adapter_spec.rb +2 -2
- data/spec/lutaml/model/xml_adapter/xml_namespace_spec.rb +6 -6
- data/spec/lutaml/model/xml_adapter_spec.rb +6 -6
- data/spec/lutaml/model/xml_mapping_rule_spec.rb +3 -3
- data/spec/lutaml/model/xml_mapping_spec.rb +26 -14
- data/spec/lutaml/model/xml_spec.rb +63 -0
- data/spec/lutaml/model/yaml_adapter_spec.rb +3 -5
- data/spec/spec_helper.rb +3 -3
- metadata +64 -59
- data/lib/lutaml/model/json_adapter/json_document.rb +0 -20
- data/lib/lutaml/model/json_adapter/json_object.rb +0 -28
- data/lib/lutaml/model/loggable.rb +0 -15
- data/lib/lutaml/model/mapping/json_mapping.rb +0 -17
- data/lib/lutaml/model/mapping/toml_mapping.rb +0 -25
- data/lib/lutaml/model/mapping/xml_mapping.rb +0 -389
- data/lib/lutaml/model/mapping/xml_mapping_rule.rb +0 -139
- data/lib/lutaml/model/mapping/yaml_mapping.rb +0 -17
- data/lib/lutaml/model/mapping.rb +0 -14
- data/lib/lutaml/model/toml_adapter/toml_document.rb +0 -20
- data/lib/lutaml/model/toml_adapter/toml_object.rb +0 -28
- data/lib/lutaml/model/toml_adapter/tomlib_adapter.rb +0 -20
- data/lib/lutaml/model/toml_adapter.rb +0 -6
- data/lib/lutaml/model/yaml_adapter/yaml_document.rb +0 -20
- data/lib/lutaml/model/yaml_adapter/yaml_object.rb +0 -28
- 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
|