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,598 @@
1
+ # = BibTeX Support in Lutaml::Model
2
+ #
3
+ # This extension adds support for BibTeX format serialization and deserialization
4
+ # to Lutaml::Model. While BibTeX is traditionally used for academic citations,
5
+ # its structured format makes it suitable for storing various types of information models.
6
+ #
7
+ # == Key Benefits of BibTeX Format
8
+ #
9
+ # - Built-in support for structured data with fields and values
10
+ # - Natural handling of collections (like authors, dependencies, ingredients)
11
+ # - Familiar citation key system for unique identification
12
+ # - Human-readable text format for easy version control
13
+ # - Extensive tooling support for parsing and manipulation
14
+ #
15
+ # == Basic Setup
16
+ #
17
+ # To use BibTeX format in your model:
18
+ #
19
+ # 1. Include necessary field classes based on your needs:
20
+ # - BibtexFieldAuthor - For handling names (authors, maintainers, manufacturers)
21
+ # - BibtexFieldYear - For handling dates and ranges
22
+ # - BibtexFieldPage - For handling numeric ranges
23
+ #
24
+ # 2. Define your model class inheriting from Lutaml::Model::Serializable
25
+ #
26
+ # 3. Register BibTeX format:
27
+ #
28
+ # Lutaml::Model::Config.register_format(
29
+ # :bibtex,
30
+ # mapping_class: BibtexMapping,
31
+ # adapter_class: BibtexAdapter
32
+ # )
33
+ #
34
+ # 4. Define BibTeX mappings using the `bibtex do` block:
35
+ #
36
+ # bibtex do
37
+ # map_entry_type to: :entry_type # Maps entry type (e.g., @article, @book)
38
+ # map_citekey to: :citekey # Maps unique identifier
39
+ # map_field "author", to: :author # Maps fields to model attributes
40
+ # map_field "title", to: :title
41
+ # # Add other field mappings as needed
42
+ # end
43
+ #
44
+ # == Mapping Methods
45
+ #
46
+ # The bibtex block supports these mapping methods:
47
+ #
48
+ # - map_entry_type: Maps the entry type (e.g., @article, @book)
49
+ # - map_citekey: Maps the unique identifier
50
+ # - map_field: Maps fields to model attributes
51
+ # Options:
52
+ # - to: Target attribute name
53
+ # - render_nil: Whether to render nil values (default: false)
54
+ #
55
+ # == Examples
56
+ #
57
+ # === 1. Traditional Academic Citation
58
+ #
59
+ # class Publication < Lutaml::Model::Serializable
60
+ # attribute :entry_type, :string, values: %w[article book inproceedings]
61
+ # attribute :citekey, :string
62
+ # attribute :author, BibtexFieldAuthor
63
+ # attribute :title, :string
64
+ # attribute :journal, :string
65
+ # attribute :year, BibtexFieldYear
66
+ #
67
+ # bibtex do
68
+ # map_entry_type to: :entry_type
69
+ # map_citekey to: :citekey
70
+ # map_field "author", to: :author
71
+ # map_field "title", to: :title
72
+ # map_field "journal", to: :journal
73
+ # map_field "year", to: :year
74
+ # end
75
+ # end
76
+ #
77
+ # Usage:
78
+ # entry = Publication.from_bibtex(bibtex_string)
79
+ # bibtex_string = entry.to_bibtex
80
+ #
81
+ # === 2. Software Components Registry
82
+ #
83
+ # BibTeX's structured format works well for tracking software components:
84
+ #
85
+ # class Component < Lutaml::Model::Serializable
86
+ # attribute :entry_type, :string, values: %w[library framework tool service]
87
+ # attribute :citekey, :string # Unique identifier
88
+ # attribute :name, :string
89
+ # attribute :maintainers, BibtexFieldAuthor, collection: true
90
+ # attribute :version, :string
91
+ # attribute :dependencies, :string, collection: true
92
+ # attribute :license, :string
93
+ #
94
+ # bibtex do
95
+ # map_entry_type to: :entry_type
96
+ # map_citekey to: :citekey
97
+ # map_field "name", to: :name
98
+ # map_field "maintainers", to: :maintainers
99
+ # map_field "version", to: :version
100
+ # map_field "dependencies", to: :dependencies
101
+ # map_field "license", to: :license
102
+ # end
103
+ # end
104
+ #
105
+ # === 3. Equipment Inventory
106
+ #
107
+ # class Equipment < Lutaml::Model::Serializable
108
+ # attribute :entry_type, :string, values: %w[machine tool vehicle equipment]
109
+ # attribute :citekey, :string # Asset ID
110
+ # attribute :model, :string
111
+ # attribute :manufacturer, BibtexFieldAuthor
112
+ # attribute :purchase_date, BibtexFieldYear
113
+ # attribute :location, :string
114
+ # attribute :maintenance_history, :string, collection: true
115
+ #
116
+ # bibtex do
117
+ # map_entry_type to: :entry_type
118
+ # map_citekey to: :citekey
119
+ # map_field "model", to: :model
120
+ # map_field "manufacturer", to: :manufacturer
121
+ # map_field "purchase_date", to: :purchase_date
122
+ # map_field "location", to: :location
123
+ # map_field "maintenance_history", to: :maintenance_history
124
+ # end
125
+ # end
126
+ #
127
+ # === 4. Recipe Database
128
+ #
129
+ # class Recipe < Lutaml::Model::Serializable
130
+ # attribute :entry_type, :string, values: %w[appetizer main dessert beverage]
131
+ # attribute :citekey, :string # Recipe ID
132
+ # attribute :name, :string
133
+ # attribute :chef, BibtexFieldAuthor
134
+ # attribute :prep_time, :string
135
+ # attribute :ingredients, :string, collection: true
136
+ # attribute :instructions, :string, collection: true
137
+ # attribute :servings, :string
138
+ #
139
+ # bibtex do
140
+ # map_entry_type to: :entry_type
141
+ # map_citekey to: :citekey
142
+ # map_field "name", to: :name
143
+ # map_field "chef", to: :chef
144
+ # map_field "prep_time", to: :prep_time
145
+ # map_field "ingredients", to: :ingredients
146
+ # map_field "instructions", to: :instructions
147
+ # map_field "servings", to: :servings
148
+ # end
149
+ # end
150
+ #
151
+ require "spec_helper"
152
+ require_relative "../../../lib/lutaml/model/serialization_adapter"
153
+
154
+ # This is a custom BibTeX adapter that can serialize and deserialize BibTeX
155
+ # entries. It is used to demonstrate how to create a custom adapter for a
156
+ # specific format.
157
+ #
158
+
159
+ module CustomBibtexAdapterSpec
160
+ class BibtexDocument
161
+ attr_reader :attributes
162
+
163
+ def initialize(attributes = {}, options = {})
164
+ @attributes = attributes
165
+ @mapping = options.delete(:mapping)
166
+ end
167
+
168
+ def [](key)
169
+ @attributes[key]
170
+ end
171
+
172
+ def []=(key, value)
173
+ @attributes[key] = value
174
+ end
175
+
176
+ def to_h
177
+ @attributes
178
+ end
179
+ end
180
+
181
+ class BibtexAdapter < BibtexDocument
182
+ def self.parse(bibtex_data, options = {})
183
+ mapping = options.delete(:mapping)
184
+ entries = bibtex_data.scan(/@(\w+)\s*{\s*([\w-]+),\s*((?:\s*\w+\s*=\s*\{.*?\},?\s*)+)\s*}/).to_h do |type, key, fields|
185
+ [type, BibtexDocumentEntry.parse(type, key, fields, mapping)]
186
+ end
187
+
188
+ new(entries)
189
+ end
190
+
191
+ def to_bibtex(*)
192
+ @attributes.map do |_type, entry|
193
+ entry.to_bibtex
194
+ end.join("\n")
195
+ end
196
+ end
197
+
198
+ class BibtexDocumentEntry
199
+ attr_reader :entry_type, :citekey, :fields, :mapping
200
+
201
+ def initialize(entry_type:, citekey:, fields:, mapping: nil)
202
+ @entry_type = entry_type
203
+ @citekey = citekey
204
+ @fields = fields
205
+
206
+ if @fields["author"].is_a?(Array)
207
+ @fields["author"] = @fields["author"].map { |a| a.gsub(/\s*,\s*/, ", ") }.join(" and ")
208
+ end
209
+ @mapping = mapping
210
+ end
211
+
212
+ def self.parse(type, key, fields, mapping)
213
+ fields_hash = fields.scan(/(\w+)\s*=\s*[{"](.+?)[}"]/m).to_h
214
+ new(
215
+ entry_type: type.downcase,
216
+ citekey: key.strip,
217
+ fields: fields_hash.transform_keys(&:downcase),
218
+ mapping: mapping,
219
+ )
220
+ end
221
+
222
+ def to_bibtex(*)
223
+ <<~BIBTEX
224
+ @#{entry_type}{#{citekey},
225
+ #{fields.compact.map { |k, v| "#{k} = {#{v}}" }.join(",\n ")}
226
+ }
227
+ BIBTEX
228
+ end
229
+ end
230
+
231
+ class BibtexMappingRule < Lutaml::Model::MappingRule
232
+ # Can be :entry_type, :citekey, or :field
233
+ attr_reader :field_type
234
+
235
+ def initialize(
236
+ name,
237
+ to:,
238
+ render_nil: false,
239
+ render_default: false,
240
+ with: {},
241
+ delegate: nil,
242
+ field_type: :field,
243
+ transform: {}
244
+ )
245
+ super(name, to: to, render_nil: render_nil, render_default: render_default,
246
+ with: with, delegate: delegate, transform: transform)
247
+ @field_type = field_type
248
+ end
249
+
250
+ def entry_type?
251
+ field_type == :entry_type
252
+ end
253
+
254
+ def citekey?
255
+ field_type == :citekey
256
+ end
257
+
258
+ def deep_dup
259
+ self.class.new(
260
+ name.dup,
261
+ to: to.dup,
262
+ render_nil: render_nil.dup,
263
+ with: Utils.deep_dup(custom_methods),
264
+ delegate: delegate,
265
+ field_type: field_type,
266
+ transform: Utils.deep_dup(transform),
267
+ )
268
+ end
269
+ end
270
+
271
+ class BibtexMapping < Lutaml::Model::Mapping
272
+ attr_reader :mappings
273
+
274
+ def initialize
275
+ super
276
+ @mappings = []
277
+ end
278
+
279
+ def map_entry_type(to:)
280
+ add_mapping("__entry_type", to, field_type: :entry_type)
281
+ end
282
+
283
+ def map_citekey(to:)
284
+ add_mapping("__citekey", to, field_type: :citekey)
285
+ end
286
+
287
+ def map_field(name, to:, render_nil: false)
288
+ add_mapping(name, to, field_type: :field, render_nil: render_nil)
289
+ end
290
+
291
+ def add_mapping(name, to, **options)
292
+ # validate!(name, to, {})
293
+ @mappings << BibtexMappingRule.new(name, to: to, **options)
294
+ end
295
+
296
+ def mapping_for_field(field)
297
+ @mappings.find { |m| m.field_type == field }
298
+ end
299
+
300
+ def validate_mapping
301
+ entry_type = @mappings.find { |m| m.field_type == :entry_type }
302
+ raise "Entry type mapping is required" unless entry_type
303
+
304
+ cite_key = @mappings.find { |m| m.field_type == :citekey }
305
+ raise "Cite key mapping is required" unless cite_key
306
+ end
307
+ end
308
+
309
+ class BibtexTransform < Lutaml::Model::Transform
310
+ def self.data_to_model(context, data, _format, _options = {})
311
+ new(context).data_to_model(data)
312
+ end
313
+
314
+ def self.model_to_data(context, model, _format, _options = {})
315
+ new(context).model_to_data(model)
316
+ end
317
+
318
+ # Assume we have a method `model_class` set at the Lutaml::Model::Mapping level
319
+ # BibtexDocumentEntry object
320
+ def data_to_model(data)
321
+ mappings = context.mappings_for(:bibtex)
322
+
323
+ data.attributes.map do |_type, entry|
324
+ bibtex_entry = model_class.new
325
+
326
+ mappings.mappings.map do |mapping|
327
+ attribute = attributes[mapping.to]
328
+ field_value = if mapping.entry_type?
329
+ entry.entry_type
330
+ elsif mapping.citekey?
331
+ entry.citekey
332
+ else
333
+ entry.fields[mapping.name]
334
+ end
335
+
336
+ if field_value
337
+ bibtex_entry.public_send(
338
+ :"#{mapping.to}=",
339
+ attribute.type.from_bibtex(field_value),
340
+ )
341
+ end
342
+ end
343
+
344
+ bibtex_entry
345
+ end
346
+ end
347
+
348
+ def model_to_data(model) # a BibtexEntry object
349
+ entry_type = model.entry_type
350
+ citekey = model.citekey
351
+ mapping = context.mappings_for(:bibtex)
352
+
353
+ fields = mapping.mappings.each_with_object({}) do |m, acc|
354
+ next if %i[entry_type citekey].include?(m.field_type)
355
+
356
+ attribute = attributes[m.to]
357
+
358
+ acc[m.name] = if attribute.collection?
359
+ model.send(m.to).map(&:to_bibtex)
360
+ elsif model.send(m.to).respond_to?(:to_bibtex)
361
+ model.send(m.to).to_bibtex
362
+ else
363
+ model.send(m.to)
364
+ end
365
+ end
366
+
367
+ { entry_type: BibtexDocumentEntry.new(
368
+ entry_type: entry_type,
369
+ citekey: citekey,
370
+ fields: fields,
371
+ ) }
372
+ end
373
+ end
374
+
375
+ # Define BibTeX field classes
376
+ class BibtexFieldPage < Lutaml::Model::Serializable
377
+ attribute :first, :string
378
+ attribute :last, :string
379
+
380
+ def self.from_bibtex(value)
381
+ if value.include?("--")
382
+ first, last = value.split("--")
383
+ BibtexFieldPage.new(first: first, last: last)
384
+ else
385
+ BibtexFieldPage.new(first: value)
386
+ end
387
+ end
388
+
389
+ def to_bibtex
390
+ first && last ? "#{first}--#{last}" : (first || last || "")
391
+ end
392
+ end
393
+
394
+ class BibtexFieldAuthor < Lutaml::Model::Serializable
395
+ attribute :given, :string # First name
396
+ attribute :family, :string # Last name
397
+ attribute :particle, :string # Particle (von, van, de, etc.)
398
+ attribute :suffix, :string # Suffix (Jr., III, etc.)
399
+
400
+ def self.from_bibtex(value)
401
+ parts = value.split(/\s+and\s+/)
402
+ parts.map do |part|
403
+ given, family = part.split(/\s*,\s*/)
404
+ particle, family = family.split(/\s+/) if family&.include?(" ")
405
+ suffix = family.split(/\s+/).last if family&.include?(" ")
406
+ BibtexFieldAuthor.new(given: given, family: family, particle: particle, suffix: suffix)
407
+ end
408
+ end
409
+
410
+ def to_bibtex
411
+ [
412
+ particle,
413
+ family,
414
+ ",",
415
+ suffix,
416
+ given,
417
+ ].compact.join(" ")
418
+ end
419
+ end
420
+
421
+ class BibtexFieldAuthorCollection < Lutaml::Model::Serializable
422
+ attribute :authors, BibtexFieldAuthor, collection: true
423
+
424
+ # BibTeX uses "and" to separate authors
425
+ def self.from_bibtex(value)
426
+ authors = value.split(/\s+and\s+/)
427
+
428
+ authors = authors.map do |author|
429
+ BibtexFieldAuthor.from_bibtex(author)
430
+ end
431
+
432
+ new(authors: authors)
433
+ end
434
+
435
+ def to_bibtex
436
+ authors.map(&:to_bibtex).join(" and ")
437
+ end
438
+ end
439
+
440
+ class BibtexFieldYear < Lutaml::Model::Serializable
441
+ attribute :from, :string
442
+ attribute :to, :string
443
+
444
+ def self.from_bibtex(value)
445
+ # If the year is a range, split it into from and to parts
446
+ # Otherwise, set the from part and leave the to part empty
447
+ # BibtexFieldYear.
448
+ if value.include?("--")
449
+ from, to = value.split("--")
450
+ BibtexFieldYear.new(from: from, to: to)
451
+ else
452
+ BibtexFieldYear.new(from: value)
453
+ end
454
+ end
455
+
456
+ def to_bibtex
457
+ from && to ? "#{from}--#{to}" : (from || to || "")
458
+ end
459
+ end
460
+
461
+ # Register BibTeX format
462
+ Lutaml::Model::FormatRegistry.register(
463
+ :bibtex,
464
+ mapping_class: BibtexMapping,
465
+ adapter_class: BibtexAdapter,
466
+ transformer: BibtexTransform,
467
+ )
468
+
469
+ # Define BibTeX entry class
470
+ class BibtexEntry < Lutaml::Model::Serializable
471
+ attribute :entry_type, :string, values: %w[
472
+ article book inproceedings conference phdthesis
473
+ mastersthesis techreport manual misc
474
+ ]
475
+ attribute :citekey, :string
476
+ attribute :author, BibtexFieldAuthor, collection: true
477
+ attribute :title, :string
478
+ attribute :journal, :string
479
+ attribute :year, BibtexFieldYear
480
+ attribute :volume, :string
481
+ attribute :number, :string
482
+ attribute :publisher, :string
483
+ attribute :address, :string
484
+ attribute :url, :string
485
+ attribute :pages, BibtexFieldPage
486
+
487
+ # Define BibTeX mappings
488
+ bibtex do
489
+ map_entry_type to: :entry_type
490
+ map_citekey to: :citekey
491
+ map_field "author", to: :author
492
+ map_field "title", to: :title
493
+ map_field "journal", to: :journal
494
+ map_field "year", to: :year
495
+ map_field "volume", to: :volume
496
+ map_field "number", to: :number, render_nil: true
497
+ map_field "pages", to: :pages
498
+ map_field "publisher", to: :publisher
499
+ map_field "address", to: :address
500
+ map_field "url", to: :url
501
+ end
502
+ end
503
+ end
504
+
505
+ RSpec.describe "Custom BibTeX adapter" do
506
+ let(:article) do
507
+ CustomBibtexAdapterSpec::BibtexEntry.new(
508
+ entry_type: "book",
509
+ citekey: "schenck1997",
510
+ title: "The EXPRESS way",
511
+ author: [
512
+ CustomBibtexAdapterSpec::BibtexFieldAuthor.new(given: "Doug", family: "Schenck"),
513
+ CustomBibtexAdapterSpec::BibtexFieldAuthor.new(given: "Peter", family: "Wilson"),
514
+ ],
515
+ year: CustomBibtexAdapterSpec::BibtexFieldYear.new(from: "1997"),
516
+ publisher: "Addison-Wesley",
517
+ address: "Reading, Massachusetts",
518
+ pages: "1--100",
519
+ )
520
+ end
521
+
522
+ let(:bibtex_string) do
523
+ <<~BIBTEX
524
+ @book{schenck1997,
525
+ author = {Schenck, Doug and Wilson, Peter},
526
+ title = {The EXPRESS way},
527
+ year = {1997},
528
+ pages = {1--100},
529
+ publisher = {Addison-Wesley},
530
+ address = {Reading, Massachusetts}
531
+ }
532
+ BIBTEX
533
+ end
534
+
535
+ describe "#to_bibtex" do
536
+ it "serializes to BibTeX format" do
537
+ expect(article.to_bibtex.gsub(/\s+/, " ").strip).to eq(
538
+ bibtex_string.gsub(/\s+/, " ").strip,
539
+ )
540
+ end
541
+ end
542
+
543
+ describe ".from_bibtex" do
544
+ let(:bibtex_string) do
545
+ <<~BIBTEX
546
+ @book{schenck1997,
547
+ title = {The EXPRESS way},
548
+ author = {Doug, Schenck and Peter, Wilson},
549
+ year = {1997},
550
+ publisher = {Addison-Wesley},
551
+ address = {Reading, Massachusetts},
552
+ pages = {1--100}
553
+ }
554
+
555
+ @misc{iso10303-11,
556
+ author = {ISO/TC 184/SC 4},
557
+ title = {Industrial automation systems and integration -- Product data representation and exchange -- Part 11: Description methods: The EXPRESS language reference manual},
558
+ year = {2004},
559
+ url = {https://www.iso.org/standard/38051.html},
560
+ publisher = {ISO},
561
+ address = {Geneva, Switzerland}
562
+ }
563
+ BIBTEX
564
+ end
565
+
566
+ it "deserializes from BibTeX format" do
567
+ result = CustomBibtexAdapterSpec::BibtexEntry.from_bibtex(bibtex_string)
568
+
569
+ expect(result.size).to eq(2)
570
+
571
+ result[0].tap do |book|
572
+ expect(book.entry_type).to eq("book")
573
+ expect(book.citekey).to eq("schenck1997")
574
+ expect(book.title).to eq("The EXPRESS way")
575
+ expect(book.author.size).to eq(2)
576
+ expect(book.author[0].given).to eq("Doug")
577
+ expect(book.author[0].family).to eq("Schenck")
578
+ expect(book.author[1].given).to eq("Peter")
579
+ expect(book.author[1].family).to eq("Wilson")
580
+ expect(book.year.from).to eq("1997")
581
+ expect(book.publisher).to eq("Addison-Wesley")
582
+ expect(book.address).to eq("Reading, Massachusetts")
583
+ end
584
+
585
+ result[1].tap do |misc|
586
+ expect(misc.entry_type).to eq("misc")
587
+ expect(misc.citekey).to eq("iso10303-11")
588
+ expect(misc.title).to eq("Industrial automation systems and integration -- Product data representation and exchange -- Part 11: Description methods: The EXPRESS language reference manual")
589
+ expect(misc.author.size).to eq(1)
590
+ expect(misc.author[0].given).to eq("ISO/TC 184/SC 4")
591
+ expect(misc.year.from).to eq("2004")
592
+ expect(misc.url).to eq("https://www.iso.org/standard/38051.html")
593
+ expect(misc.publisher).to eq("ISO")
594
+ expect(misc.address).to eq("Geneva, Switzerland")
595
+ end
596
+ end
597
+ end
598
+ end