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.
Files changed (110) hide show
  1. checksums.yaml +7 -0
  2. data/.github/workflows/main.yml +27 -0
  3. data/.gitignore +12 -0
  4. data/.rspec +3 -0
  5. data/.rubocop.yml +10 -0
  6. data/.rubocop_todo.yml +450 -0
  7. data/CLAUDE.md +57 -0
  8. data/CODE_OF_CONDUCT.md +132 -0
  9. data/CORRECTED_HTTP_CACHE_IMPLEMENTATION.md +209 -0
  10. data/CORRECTED_HTTP_CACHE_PLAN.md +164 -0
  11. data/Gemfile +15 -0
  12. data/Gemfile.lock +220 -0
  13. data/README.adoc +1430 -0
  14. data/Rakefile +12 -0
  15. data/TODO.impl/0-lutaml-store-self-quality.md +112 -0
  16. data/TODO.impl/1-lutaml-hal-migration.md +60 -0
  17. data/TODO.impl/2-glossarist-migration.md +359 -0
  18. data/TODO.impl/3-lutaml-jsonschema-migration.md +273 -0
  19. data/bin/console +11 -0
  20. data/bin/setup +8 -0
  21. data/demo/Gemfile +15 -0
  22. data/demo/Gemfile.lock +61 -0
  23. data/demo/README.adoc +301 -0
  24. data/demo/data/vcards/co/contact_10_thompson.data +1 -0
  25. data/demo/data/vcards/co/contact_10_thompson.meta +1 -0
  26. data/demo/data/vcards/co/contact_1_doe.data +1 -0
  27. data/demo/data/vcards/co/contact_1_doe.meta +1 -0
  28. data/demo/data/vcards/co/contact_2_smith.data +1 -0
  29. data/demo/data/vcards/co/contact_2_smith.meta +1 -0
  30. data/demo/data/vcards/co/contact_3_johnson.data +1 -0
  31. data/demo/data/vcards/co/contact_3_johnson.meta +1 -0
  32. data/demo/data/vcards/co/contact_4_garcia.data +1 -0
  33. data/demo/data/vcards/co/contact_4_garcia.meta +1 -0
  34. data/demo/data/vcards/co/contact_5_wilson.data +1 -0
  35. data/demo/data/vcards/co/contact_5_wilson.meta +1 -0
  36. data/demo/data/vcards/co/contact_6_brown.data +1 -0
  37. data/demo/data/vcards/co/contact_6_brown.meta +1 -0
  38. data/demo/data/vcards/co/contact_7_davis.data +1 -0
  39. data/demo/data/vcards/co/contact_7_davis.meta +1 -0
  40. data/demo/data/vcards/co/contact_8_anderson.data +1 -0
  41. data/demo/data/vcards/co/contact_8_anderson.meta +1 -0
  42. data/demo/data/vcards/co/contact_9_taylor.data +1 -0
  43. data/demo/data/vcards/co/contact_9_taylor.meta +1 -0
  44. data/demo/data/vcards.db +0 -0
  45. data/demo/pottery_class_demo.rb +164 -0
  46. data/demo/vcard_models.rb +140 -0
  47. data/demo/vcard_store_demo.rb +526 -0
  48. data/lib/lutaml/store/adapter/base.rb +65 -0
  49. data/lib/lutaml/store/adapter/filesystem.rb +288 -0
  50. data/lib/lutaml/store/adapter/memory.rb +225 -0
  51. data/lib/lutaml/store/adapter/sqlite.rb +193 -0
  52. data/lib/lutaml/store/adapter.rb +12 -0
  53. data/lib/lutaml/store/attribute_updater.rb +198 -0
  54. data/lib/lutaml/store/basic_store.rb +190 -0
  55. data/lib/lutaml/store/cache.rb +108 -0
  56. data/lib/lutaml/store/cache_store.rb +282 -0
  57. data/lib/lutaml/store/composite_model_handler.rb +169 -0
  58. data/lib/lutaml/store/compression.rb +137 -0
  59. data/lib/lutaml/store/config.rb +178 -0
  60. data/lib/lutaml/store/database_store.rb +425 -0
  61. data/lib/lutaml/store/events.rb +92 -0
  62. data/lib/lutaml/store/format/base.rb +33 -0
  63. data/lib/lutaml/store/format/json.rb +25 -0
  64. data/lib/lutaml/store/format/jsonl.rb +37 -0
  65. data/lib/lutaml/store/format/marshal_format.rb +37 -0
  66. data/lib/lutaml/store/format/yaml.rb +29 -0
  67. data/lib/lutaml/store/format/yamls.rb +35 -0
  68. data/lib/lutaml/store/format.rb +33 -0
  69. data/lib/lutaml/store/http_cache.rb +279 -0
  70. data/lib/lutaml/store/http_cache_config.rb +53 -0
  71. data/lib/lutaml/store/http_cache_entry.rb +69 -0
  72. data/lib/lutaml/store/http_header_processor.rb +175 -0
  73. data/lib/lutaml/store/integrity.rb +102 -0
  74. data/lib/lutaml/store/model_registration.rb +75 -0
  75. data/lib/lutaml/store/model_registry.rb +123 -0
  76. data/lib/lutaml/store/model_serializer.rb +69 -0
  77. data/lib/lutaml/store/monitor.rb +192 -0
  78. data/lib/lutaml/store/storage_key.rb +40 -0
  79. data/lib/lutaml/store/version.rb +7 -0
  80. data/lib/lutaml/store.rb +41 -0
  81. data/lutaml-store.gemspec +35 -0
  82. data/plan.adoc +606 -0
  83. data/sig/lutaml/store.rbs +6 -0
  84. data/spec/lutaml/store/adapter_interface_spec.rb +89 -0
  85. data/spec/lutaml/store/anti_pattern_guard_spec.rb +35 -0
  86. data/spec/lutaml/store/anti_pattern_spec.rb +78 -0
  87. data/spec/lutaml/store/autoload_spec.rb +34 -0
  88. data/spec/lutaml/store/cache_store_spec.rb +271 -0
  89. data/spec/lutaml/store/compression_spec.rb +78 -0
  90. data/spec/lutaml/store/config_enhanced_spec.rb +158 -0
  91. data/spec/lutaml/store/corrected_http_cache_integration_spec.rb +336 -0
  92. data/spec/lutaml/store/custom_serializer_spec.rb +108 -0
  93. data/spec/lutaml/store/database_store_spec.rb +279 -0
  94. data/spec/lutaml/store/file_io_spec.rb +219 -0
  95. data/spec/lutaml/store/format_round_trip_spec.rb +110 -0
  96. data/spec/lutaml/store/format_spec.rb +70 -0
  97. data/spec/lutaml/store/http_cache_entry_spec.rb +203 -0
  98. data/spec/lutaml/store/http_cache_hal_integration_spec.rb +404 -0
  99. data/spec/lutaml/store/http_cache_spec.rb +422 -0
  100. data/spec/lutaml/store/http_header_processor_spec.rb +290 -0
  101. data/spec/lutaml/store/import_spec.rb +90 -0
  102. data/spec/lutaml/store/integrity_spec.rb +157 -0
  103. data/spec/lutaml/store/key_collision_serializer_spec.rb +98 -0
  104. data/spec/lutaml/store/load_save_spec.rb +107 -0
  105. data/spec/lutaml/store/lutaml_model_integration_spec.rb +291 -0
  106. data/spec/lutaml/store/model_serializer_spec.rb +140 -0
  107. data/spec/lutaml/store/store_spec.rb +182 -0
  108. data/spec/lutaml/store_spec.rb +21 -0
  109. data/spec/spec_helper.rb +16 -0
  110. 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"}}
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