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,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
|