lutaml-store 0.1.1
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 +7 -0
- data/.github/workflows/main.yml +27 -0
- data/.gitignore +12 -0
- data/.rspec +3 -0
- data/.rubocop.yml +10 -0
- data/.rubocop_todo.yml +450 -0
- data/CLAUDE.md +57 -0
- data/CODE_OF_CONDUCT.md +132 -0
- data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +209 -0
- data/CORRECTED_HTTP_CACHE_PLAN.md +164 -0
- data/Gemfile +15 -0
- data/Gemfile.lock +220 -0
- data/README.adoc +1430 -0
- data/Rakefile +12 -0
- data/TODO.impl/0-lutaml-store-self-quality.md +112 -0
- data/TODO.impl/1-lutaml-hal-migration.md +60 -0
- data/TODO.impl/2-glossarist-migration.md +359 -0
- data/TODO.impl/3-lutaml-jsonschema-migration.md +273 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/demo/Gemfile +15 -0
- data/demo/Gemfile.lock +61 -0
- data/demo/README.adoc +301 -0
- data/demo/data/vcards/co/contact_10_thompson.data +1 -0
- data/demo/data/vcards/co/contact_10_thompson.meta +1 -0
- data/demo/data/vcards/co/contact_1_doe.data +1 -0
- data/demo/data/vcards/co/contact_1_doe.meta +1 -0
- data/demo/data/vcards/co/contact_2_smith.data +1 -0
- data/demo/data/vcards/co/contact_2_smith.meta +1 -0
- data/demo/data/vcards/co/contact_3_johnson.data +1 -0
- data/demo/data/vcards/co/contact_3_johnson.meta +1 -0
- data/demo/data/vcards/co/contact_4_garcia.data +1 -0
- data/demo/data/vcards/co/contact_4_garcia.meta +1 -0
- data/demo/data/vcards/co/contact_5_wilson.data +1 -0
- data/demo/data/vcards/co/contact_5_wilson.meta +1 -0
- data/demo/data/vcards/co/contact_6_brown.data +1 -0
- data/demo/data/vcards/co/contact_6_brown.meta +1 -0
- data/demo/data/vcards/co/contact_7_davis.data +1 -0
- data/demo/data/vcards/co/contact_7_davis.meta +1 -0
- data/demo/data/vcards/co/contact_8_anderson.data +1 -0
- data/demo/data/vcards/co/contact_8_anderson.meta +1 -0
- data/demo/data/vcards/co/contact_9_taylor.data +1 -0
- data/demo/data/vcards/co/contact_9_taylor.meta +1 -0
- data/demo/data/vcards.db +0 -0
- data/demo/pottery_class_demo.rb +164 -0
- data/demo/vcard_models.rb +140 -0
- data/demo/vcard_store_demo.rb +526 -0
- data/lib/lutaml/store/adapter/base.rb +65 -0
- data/lib/lutaml/store/adapter/filesystem.rb +288 -0
- data/lib/lutaml/store/adapter/memory.rb +225 -0
- data/lib/lutaml/store/adapter/sqlite.rb +193 -0
- data/lib/lutaml/store/adapter.rb +12 -0
- data/lib/lutaml/store/attribute_updater.rb +198 -0
- data/lib/lutaml/store/basic_store.rb +190 -0
- data/lib/lutaml/store/cache.rb +108 -0
- data/lib/lutaml/store/cache_store.rb +282 -0
- data/lib/lutaml/store/composite_model_handler.rb +169 -0
- data/lib/lutaml/store/compression.rb +137 -0
- data/lib/lutaml/store/config.rb +178 -0
- data/lib/lutaml/store/database_store.rb +425 -0
- data/lib/lutaml/store/events.rb +92 -0
- data/lib/lutaml/store/format/base.rb +33 -0
- data/lib/lutaml/store/format/json.rb +25 -0
- data/lib/lutaml/store/format/jsonl.rb +37 -0
- data/lib/lutaml/store/format/marshal_format.rb +37 -0
- data/lib/lutaml/store/format/yaml.rb +29 -0
- data/lib/lutaml/store/format/yamls.rb +35 -0
- data/lib/lutaml/store/format.rb +33 -0
- data/lib/lutaml/store/http_cache.rb +279 -0
- data/lib/lutaml/store/http_cache_config.rb +53 -0
- data/lib/lutaml/store/http_cache_entry.rb +69 -0
- data/lib/lutaml/store/http_header_processor.rb +175 -0
- data/lib/lutaml/store/integrity.rb +102 -0
- data/lib/lutaml/store/model_registration.rb +75 -0
- data/lib/lutaml/store/model_registry.rb +123 -0
- data/lib/lutaml/store/model_serializer.rb +69 -0
- data/lib/lutaml/store/monitor.rb +192 -0
- data/lib/lutaml/store/storage_key.rb +40 -0
- data/lib/lutaml/store/version.rb +7 -0
- data/lib/lutaml/store.rb +41 -0
- data/lutaml-store.gemspec +35 -0
- data/plan.adoc +606 -0
- data/sig/lutaml/store.rbs +6 -0
- data/spec/lutaml/store/adapter_interface_spec.rb +89 -0
- data/spec/lutaml/store/anti_pattern_guard_spec.rb +35 -0
- data/spec/lutaml/store/anti_pattern_spec.rb +78 -0
- data/spec/lutaml/store/autoload_spec.rb +34 -0
- data/spec/lutaml/store/cache_store_spec.rb +271 -0
- data/spec/lutaml/store/compression_spec.rb +78 -0
- data/spec/lutaml/store/config_enhanced_spec.rb +158 -0
- data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +336 -0
- data/spec/lutaml/store/custom_serializer_spec.rb +108 -0
- data/spec/lutaml/store/database_store_spec.rb +279 -0
- data/spec/lutaml/store/file_io_spec.rb +219 -0
- data/spec/lutaml/store/format_round_trip_spec.rb +110 -0
- data/spec/lutaml/store/format_spec.rb +70 -0
- data/spec/lutaml/store/http_cache_entry_spec.rb +203 -0
- data/spec/lutaml/store/http_cache_hal_integration_spec.rb +404 -0
- data/spec/lutaml/store/http_cache_spec.rb +422 -0
- data/spec/lutaml/store/http_header_processor_spec.rb +290 -0
- data/spec/lutaml/store/import_spec.rb +90 -0
- data/spec/lutaml/store/integrity_spec.rb +157 -0
- data/spec/lutaml/store/key_collision_serializer_spec.rb +98 -0
- data/spec/lutaml/store/load_save_spec.rb +107 -0
- data/spec/lutaml/store/lutaml_model_integration_spec.rb +291 -0
- data/spec/lutaml/store/model_serializer_spec.rb +140 -0
- data/spec/lutaml/store/store_spec.rb +182 -0
- data/spec/lutaml/store_spec.rb +21 -0
- data/spec/spec_helper.rb +16 -0
- metadata +166 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{:version=>"4.0", :fn=>"Maria Garcia", :n=>{:family=>"Garcia", :given=>"Maria", :additional=>nil, :prefix=>nil, :suffix=>nil}, :tel=>[{:value=>"+1-555-0104", :type=>"mobile", :pref=>nil}], :email=>["maria.garcia@startup.com"], :org=>"Innovation Labs", :bday=>{:value=>"1988-02-14"}, :uid=>"922c2f06-cabe-41e5-b414-cc2e04609395"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"integrity":{"checksum":"93d258d2a737f634e6e33e1ec018c0c3d11462b5e39e245f29f09ba5e5b233d3","algorithm":"sha256","size":328,"created_at":"2025-07-11T00:01:58Z","version":"1.0"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{:version=>"4.0", :fn=>"David Wilson", :n=>{:family=>"Wilson", :given=>"David", :additional=>nil, :prefix=>nil, :suffix=>nil}, :tel=>[{:value=>"+1-555-0105", :type=>"work", :pref=>nil}], :email=>["david.wilson@consulting.com"], :org=>"Wilson Consulting", :bday=>{:value=>"1982-09-30"}, :uid=>"e16ad469-d63f-4e6b-b8fd-f04f9302c282"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"integrity":{"checksum":"b3b9393657f525d0e9fb141552f4028e3178d07af3e1254bf0b96914a9c4e05c","algorithm":"sha256","size":331,"created_at":"2025-07-11T00:01:58Z","version":"1.0"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{:version=>"4.0", :fn=>"Sarah Brown", :n=>{:family=>"Brown", :given=>"Sarah", :additional=>nil, :prefix=>nil, :suffix=>nil}, :tel=>[{:value=>"+1-555-0106", :type=>"work", :pref=>nil}], :email=>["sarah.brown@university.edu", "s.brown@research.org"], :org=>"State University", :bday=>{:value=>"1979-12-03"}, :uid=>"136c8cc8-b4cc-431b-b74a-6dd0defefdfd"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"integrity":{"checksum":"3bcc15d0d646175bfe6463fa37535277daeda19554554d2055cfa1386f5cbc74","algorithm":"sha256","size":351,"created_at":"2025-07-11T00:01:58Z","version":"1.0"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{:version=>"4.0", :fn=>"Michael Davis", :n=>{:family=>"Davis", :given=>"Michael", :additional=>nil, :prefix=>nil, :suffix=>nil}, :tel=>[{:value=>"+1-555-0107", :type=>"mobile", :pref=>nil}], :email=>["mike.davis@agency.com"], :org=>"Creative Agency", :bday=>{:value=>"1992-05-18"}, :uid=>"1bdba76a-276e-43ad-81bb-383615b1be86"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"integrity":{"checksum":"6987b68c76e51ad930e5130ebaecb92c4fae96f1ce1a330cc1fe3ad9d026f2fe","algorithm":"sha256","size":327,"created_at":"2025-07-11T00:01:58Z","version":"1.0"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{:version=>"4.0", :fn=>"Lisa Anderson", :n=>{:family=>"Anderson", :given=>"Lisa", :additional=>nil, :prefix=>nil, :suffix=>nil}, :tel=>[{:value=>"+1-555-0108", :type=>"work", :pref=>nil}], :email=>["lisa.anderson@finance.com"], :org=>"Financial Services Inc", :bday=>{:value=>"1986-08-25"}, :uid=>"245aa557-bb68-4eb2-9e9e-08f7978bf760"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"integrity":{"checksum":"104ec023a4cc8c0df05ff1ba91b3d5f5dc7313f36b49e18e34f530d7e86a3ae2","algorithm":"sha256","size":336,"created_at":"2025-07-11T00:01:58Z","version":"1.0"}}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{:version=>"4.0", :fn=>"James Taylor", :n=>{:family=>"Taylor", :given=>"James", :additional=>nil, :prefix=>nil, :suffix=>nil}, :tel=>[{:value=>"+1-555-0109", :type=>"mobile", :pref=>nil}], :email=>["james.taylor@music.com", "jtaylor@personal.net"], :org=>"Music Production", :bday=>{:value=>"1983-01-12"}, :uid=>"0816b9c0-7957-43b2-8a9c-b2df8b2e974d"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"integrity":{"checksum":"84a96a3a0adabe9c13a9bb67e65c3ea675cc8ec36536a34b8293eadd9f9b6b9a","algorithm":"sha256","size":351,"created_at":"2025-07-11T00:01:58Z","version":"1.0"}}
|
data/demo/data/vcards.db
ADDED
|
Binary file
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "lutaml/model"
|
|
4
|
+
require_relative "lib/lutaml/store"
|
|
5
|
+
|
|
6
|
+
class Studio < Lutaml::Model::Serializable
|
|
7
|
+
attribute :studio_key, :string
|
|
8
|
+
attribute :name, :string
|
|
9
|
+
attribute :location, :string
|
|
10
|
+
attribute :_class, :string, default: -> { "Studio" }, polymorphic_class: true
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# CeramicStudio is a specialization of Studio
|
|
14
|
+
class CeramicStudio < Studio
|
|
15
|
+
attribute :clay_type, :string
|
|
16
|
+
# Override the _class attribute to indicate this is a CeramicStudio
|
|
17
|
+
attribute :_class, :string, default: -> { "CeramicStudio" }
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class PotteryClass < Lutaml::Model::Serializable
|
|
21
|
+
# the :studio attribute should accept Studio and CeramicStudio
|
|
22
|
+
attribute :studio, Studio, polymorphic: true
|
|
23
|
+
attribute :class_id, :string
|
|
24
|
+
attribute :description, :string
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
puts "=== Creating pottery classes ==="
|
|
28
|
+
p = [
|
|
29
|
+
PotteryClass.new(
|
|
30
|
+
class_id: "pottery_class",
|
|
31
|
+
studio: Studio.new(studio_key: "pottery_studio", name: "Pottery studio"),
|
|
32
|
+
description: "A class for pottery making"
|
|
33
|
+
),
|
|
34
|
+
PotteryClass.new(
|
|
35
|
+
class_id: "clay_class",
|
|
36
|
+
studio: Studio.new(studio_key: "clay_place", name: "Clay place"),
|
|
37
|
+
description: "A class for clay modeling"
|
|
38
|
+
),
|
|
39
|
+
PotteryClass.new(
|
|
40
|
+
class_id: "artisan_class",
|
|
41
|
+
studio: Studio.new(studio_key: "artisan_studio", name: "Artisan studio"),
|
|
42
|
+
description: "A class for artisan pottery"
|
|
43
|
+
)
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
puts "=== Creating store ==="
|
|
47
|
+
# Create a simple in-memory store
|
|
48
|
+
store = Lutaml::Store.new(
|
|
49
|
+
# This creates an in-memory store
|
|
50
|
+
adapter: :memory,
|
|
51
|
+
# This registers the models that will be stored in the store with their unique
|
|
52
|
+
# keys.
|
|
53
|
+
# The key is used to identify the model in the store and is used to fetch,
|
|
54
|
+
# update, or delete the model.
|
|
55
|
+
# If an inner model is not registered, it will only be persisted as a part of
|
|
56
|
+
# the parent model, but not as a separate entity.
|
|
57
|
+
# If an inner model that is registered gets updated independently via its
|
|
58
|
+
# unique key, the parent model will not be updated in terms of reference to
|
|
59
|
+
# the inner model but the realization of the parent model will reflect the
|
|
60
|
+
# changes because it is a composite model.
|
|
61
|
+
models: [
|
|
62
|
+
{
|
|
63
|
+
model: PotteryClass,
|
|
64
|
+
key: :class_id
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
model: Studio,
|
|
68
|
+
key: :studio_key,
|
|
69
|
+
polymorphic_class_key: :_class
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
model: CeramicStudio,
|
|
73
|
+
key: :studio_key,
|
|
74
|
+
polymorphic_class_key: :_class
|
|
75
|
+
}
|
|
76
|
+
]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
puts "=== Saving pottery classes ==="
|
|
80
|
+
store.save(p)
|
|
81
|
+
|
|
82
|
+
puts "=== Fetching pottery class ==="
|
|
83
|
+
p1 = store.fetch(model: PotteryClass, class_id: "pottery_class")
|
|
84
|
+
puts "p1.studio: #{p1.studio.inspect}"
|
|
85
|
+
# => #<Studio:0x00007f8c8b0a4b80 @name="Pottery studio", @location=nil>
|
|
86
|
+
puts "p1.studio.name: #{p1.studio.name}"
|
|
87
|
+
|
|
88
|
+
puts "=== Updating pottery class description ==="
|
|
89
|
+
# Updating the pottery class to a different description
|
|
90
|
+
store.update(
|
|
91
|
+
model: PotteryClass,
|
|
92
|
+
class_id: "clay_class",
|
|
93
|
+
attributes: {
|
|
94
|
+
description: "A class for advanced clay modeling"
|
|
95
|
+
}
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
puts "=== Fetching updated pottery class ==="
|
|
99
|
+
# Fetching the updated pottery class
|
|
100
|
+
p2 = store.fetch(model: PotteryClass, class_id: "clay_class")
|
|
101
|
+
puts "p2.description: #{p2.description}"
|
|
102
|
+
# => "A class for advanced clay modeling"
|
|
103
|
+
|
|
104
|
+
puts "=== Updating studio location with dot notation ==="
|
|
105
|
+
# Updating the studio to a different location
|
|
106
|
+
store.update(
|
|
107
|
+
model: PotteryClass,
|
|
108
|
+
class_id: "clay_class",
|
|
109
|
+
attributes: {
|
|
110
|
+
"studio.location" => "Far enough downtown"
|
|
111
|
+
}
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
puts "=== Fetching pottery class with updated studio ==="
|
|
115
|
+
# Fetching the updated pottery class
|
|
116
|
+
p2 = store.fetch(model: PotteryClass, class_id: "clay_class")
|
|
117
|
+
puts "p2.studio.location: #{p2.studio.location}"
|
|
118
|
+
# => "Far enough downtown"
|
|
119
|
+
|
|
120
|
+
puts "=== Creating and saving CeramicStudio ==="
|
|
121
|
+
# Replacing the Studio with a CeramicStudio
|
|
122
|
+
ceramic_studio = CeramicStudio.new(
|
|
123
|
+
studio_key: "artisan_studio",
|
|
124
|
+
name: "Angelic pottery studio",
|
|
125
|
+
clay_type: "Porcelain"
|
|
126
|
+
)
|
|
127
|
+
store.save(ceramic_studio)
|
|
128
|
+
|
|
129
|
+
puts "=== Fetching artisan class (should have CeramicStudio) ==="
|
|
130
|
+
# Fetching the updated artisan class
|
|
131
|
+
p3 = store.fetch(model: PotteryClass, class_id: "artisan_class")
|
|
132
|
+
puts "p3.studio.name: #{p3.studio.name}"
|
|
133
|
+
# => "Angelic pottery studio"
|
|
134
|
+
puts "p3.studio.class: #{p3.studio.class}"
|
|
135
|
+
# => "CeramicStudio"
|
|
136
|
+
puts "p3.studio.clay_type: #{p3.studio.clay_type}"
|
|
137
|
+
# => "Porcelain"
|
|
138
|
+
|
|
139
|
+
puts "=== Updating pottery class with new CeramicStudio ==="
|
|
140
|
+
# Updating the studio to a CeramicStudio instance
|
|
141
|
+
store.update(
|
|
142
|
+
model: PotteryClass,
|
|
143
|
+
class_id: "pottery_class",
|
|
144
|
+
attributes: {
|
|
145
|
+
studio: CeramicStudio.new(
|
|
146
|
+
studio_key: "pottery_studio",
|
|
147
|
+
name: "Ceramic studio",
|
|
148
|
+
clay_type: "Stoneware"
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
puts "=== Fetching final pottery class ==="
|
|
154
|
+
# Fetching the updated pottery class
|
|
155
|
+
p1 = store.fetch(model: PotteryClass, class_id: "pottery_class")
|
|
156
|
+
puts "p1.studio: #{p1.studio.inspect}"
|
|
157
|
+
# => #<CeramicStudio:0x00007f8c8b0a4b80 @name="Ceramic studio", @clay_type="Stoneware">
|
|
158
|
+
|
|
159
|
+
puts "p1.studio.class: #{p1.studio.class}"
|
|
160
|
+
# => "CeramicStudio"
|
|
161
|
+
puts "p1.studio.clay_type: #{p1.studio.clay_type}"
|
|
162
|
+
# => "Stoneware"
|
|
163
|
+
|
|
164
|
+
puts "=== All tests completed successfully! ==="
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "securerandom"
|
|
4
|
+
require "date"
|
|
5
|
+
|
|
6
|
+
# Simplified VCard models for demo purposes
|
|
7
|
+
module VCardDemo
|
|
8
|
+
# Birthday property for vCard
|
|
9
|
+
class VcardBday
|
|
10
|
+
attr_accessor :value
|
|
11
|
+
|
|
12
|
+
def initialize(value:)
|
|
13
|
+
@value = value
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def to_s
|
|
17
|
+
value.to_s
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def to_h
|
|
21
|
+
{ value: value }
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Telephone property for vCard
|
|
26
|
+
class VcardTel
|
|
27
|
+
attr_accessor :value, :type, :pref
|
|
28
|
+
|
|
29
|
+
def initialize(value:, type: nil, pref: nil)
|
|
30
|
+
@value = value
|
|
31
|
+
@type = type
|
|
32
|
+
@pref = pref
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def to_s
|
|
36
|
+
"#{value} (#{type})"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def to_h
|
|
40
|
+
{ value: value, type: type, pref: pref }
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Structured name for vCard
|
|
45
|
+
class VcardName
|
|
46
|
+
attr_accessor :family, :given, :additional, :prefix, :suffix
|
|
47
|
+
|
|
48
|
+
def initialize(family: nil, given: nil, additional: nil, prefix: nil, suffix: nil)
|
|
49
|
+
@family = family
|
|
50
|
+
@given = given
|
|
51
|
+
@additional = additional
|
|
52
|
+
@prefix = prefix
|
|
53
|
+
@suffix = suffix
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def full_name
|
|
57
|
+
parts = [prefix, given, additional, family, suffix].compact.reject(&:empty?)
|
|
58
|
+
parts.join(" ")
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def to_s
|
|
62
|
+
full_name
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def to_h
|
|
66
|
+
{
|
|
67
|
+
family: family,
|
|
68
|
+
given: given,
|
|
69
|
+
additional: additional,
|
|
70
|
+
prefix: prefix,
|
|
71
|
+
suffix: suffix
|
|
72
|
+
}
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Main vCard class
|
|
77
|
+
class Vcard
|
|
78
|
+
attr_accessor :version, :fn, :n, :tel, :email, :org, :bday, :uid
|
|
79
|
+
|
|
80
|
+
def initialize(version: "4.0", fn: nil, n: nil, tel: [], email: [], org: nil, bday: nil, uid: nil)
|
|
81
|
+
@version = version
|
|
82
|
+
@fn = fn
|
|
83
|
+
@n = n
|
|
84
|
+
@tel = tel || []
|
|
85
|
+
@email = email || []
|
|
86
|
+
@org = org
|
|
87
|
+
@bday = bday
|
|
88
|
+
@uid = uid || SecureRandom.uuid
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def primary_email
|
|
92
|
+
email&.first
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def primary_phone
|
|
96
|
+
tel&.first
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def age
|
|
100
|
+
return nil unless bday&.value
|
|
101
|
+
|
|
102
|
+
birth_date = parse_birth_date
|
|
103
|
+
return nil unless birth_date
|
|
104
|
+
|
|
105
|
+
today = Date.today
|
|
106
|
+
age = today.year - birth_date.year
|
|
107
|
+
age -= 1 if today < birth_date.next_year(age)
|
|
108
|
+
age
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def to_s
|
|
112
|
+
"#{fn} (#{primary_email})"
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def to_h
|
|
116
|
+
{
|
|
117
|
+
version: version,
|
|
118
|
+
fn: fn,
|
|
119
|
+
n: n&.to_h,
|
|
120
|
+
tel: tel.map(&:to_h),
|
|
121
|
+
email: email,
|
|
122
|
+
org: org,
|
|
123
|
+
bday: bday&.to_h,
|
|
124
|
+
uid: uid
|
|
125
|
+
}
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
private
|
|
129
|
+
|
|
130
|
+
def parse_birth_date
|
|
131
|
+
return nil unless bday&.value
|
|
132
|
+
|
|
133
|
+
begin
|
|
134
|
+
Date.parse(bday.value.to_s)
|
|
135
|
+
rescue StandardError
|
|
136
|
+
nil
|
|
137
|
+
end
|
|
138
|
+
end
|
|
139
|
+
end
|
|
140
|
+
end
|