bibframe_ruby 0.1.0

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: fcbdeff768bfbc758d78355aa9f9de7e98a513cc0a0c63a43ac1e2b9b2c4e6bb
4
+ data.tar.gz: c2e8be5aed30fa4cb8a6bc88c5d0e70b267b3b2c3b73aa5a141fc76169696c74
5
+ SHA512:
6
+ metadata.gz: 1be2ad497cc3ba26bf0eca414bcdc27cf80aaebae69db92fb469462fec0f85cf2ad28aff7a7f3b00f993b31c408804cff391434b97de96387135c424bc2b34ad
7
+ data.tar.gz: a09548d0df9b71f46188ac47935dd1dc13e827259183ce716a8b9aac2e555a016c7a07f9cd9770c3c38957514718df3163162447d551c484ac53afcab1dfd56a
data/README.md ADDED
@@ -0,0 +1,261 @@
1
+ # BibframeRuby
2
+
3
+ A Ruby gem for parsing [BIBFRAME](https://www.loc.gov/bibframe/) data into Ruby objects. Currently supports JSON-LD, with Turtle and RDF/XML support planned.
4
+
5
+ Built on the [RDF.rb](https://github.com/ruby-rdf/rdf) ecosystem, BibframeRuby converts BIBFRAME documents into typed Ruby objects with idiomatic accessors and linked relationships.
6
+
7
+ ## Installation
8
+
9
+ Add this line to your application's Gemfile:
10
+
11
+ ```ruby
12
+ gem "bibframe_ruby"
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ ```bash
18
+ bundle install
19
+ ```
20
+
21
+ Or install it yourself as:
22
+
23
+ ```bash
24
+ gem install bibframe_ruby
25
+ ```
26
+
27
+ ## Usage
28
+
29
+ ### Parsing JSON-LD
30
+
31
+ ```ruby
32
+ require "bibframe_ruby"
33
+
34
+ # Parse a JSON-LD string
35
+ json = File.read("work.jsonld")
36
+ graph = BibframeRuby.parse(json)
37
+
38
+ # Or parse directly from a file (format detected from extension)
39
+ graph = BibframeRuby.parse_file("work.jsonld")
40
+
41
+ # Parse from a remote URI (fetches the content)
42
+ graph = BibframeRuby.parse_uri("https://id.loc.gov/resources/hubs/4076e139-793f-bb85-515c-840510066bac.jsonld")
43
+ ```
44
+
45
+ ### Accessing Resources
46
+
47
+ The returned `Graph` provides typed collections:
48
+
49
+ ```ruby
50
+ graph.works # => [BibframeRuby::Work, ...]
51
+ graph.instances # => [BibframeRuby::Instance, ...]
52
+ graph.items # => [BibframeRuby::Item, ...]
53
+ graph.hubs # => [BibframeRuby::Hub, ...]
54
+ graph.resources # => all parsed resources
55
+ ```
56
+
57
+ ### Working with a Work
58
+
59
+ ```ruby
60
+ work = graph.works.first
61
+
62
+ work.id
63
+ # => "https://dev.bcld.info/works/25305194-1115-43ab-8a7c-4ef586a1e8e5"
64
+
65
+ work.types
66
+ # => ["Monograph", "Text", "Work"]
67
+
68
+ work.title.main_title
69
+ # => "The dungeon anarchist's cookbook"
70
+
71
+ work.title.non_sort_num
72
+ # => "4"
73
+
74
+ work.language
75
+ # => "http://id.loc.gov/vocabulary/languages/eng"
76
+
77
+ work.genre_forms
78
+ # => ["http://id.loc.gov/authorities/genreForms/gf2023026123", ...]
79
+
80
+ work.summary
81
+ # => "\"Welcome to the Gun Show! The top ten list is populated..."
82
+
83
+ work.classifications.length
84
+ # => 2
85
+ ```
86
+
87
+ ### Contributions
88
+
89
+ ```ruby
90
+ contribution = work.contributions.first
91
+
92
+ contribution.primary?
93
+ # => true
94
+
95
+ contribution.role
96
+ # => "http://id.loc.gov/vocabulary/relators/aut"
97
+
98
+ contribution.agent.id
99
+ # => "http://id.loc.gov/rwo/agents/no2023085548"
100
+ ```
101
+
102
+ ### Working with an Instance
103
+
104
+ When you parse both a Work and its Instance, the relationships are automatically linked:
105
+
106
+ ```ruby
107
+ instance = graph.instances.first
108
+
109
+ instance.title.main_title
110
+ # => "The dungeon anarchist's cookbook"
111
+
112
+ instance.extent
113
+ # => "532 pages"
114
+
115
+ instance.dimensions
116
+ # => "24 cm"
117
+
118
+ instance.edition_statement
119
+ # => "First Ace edition"
120
+
121
+ instance.publication_statement
122
+ # => "New York: Ace, 2024"
123
+
124
+ # Bidirectional linking
125
+ instance.work.title.main_title
126
+ # => "The dungeon anarchist's cookbook"
127
+
128
+ work.instances.first == instance
129
+ # => true
130
+ ```
131
+
132
+ ### Working with a Hub
133
+
134
+ Hubs are abstract resources that bridge between Works — commonly used for authority-linked title/author combinations.
135
+
136
+ ```ruby
137
+ graph = BibframeRuby.parse_file("hub.jsonld")
138
+ hub = graph.hubs.first
139
+
140
+ hub.id
141
+ # => "http://id.loc.gov/resources/hubs/4076e139-793f-bb85-515c-840510066bac"
142
+
143
+ hub.types
144
+ # => ["Work", "Hub", "Series"]
145
+
146
+ hub.title.main_title
147
+ # => "Dungeon crawler Carl (Series)"
148
+
149
+ hub.contributions.first.primary?
150
+ # => true
151
+
152
+ hub.relations.length
153
+ # => 1
154
+ ```
155
+
156
+ ### Identifiers
157
+
158
+ ```ruby
159
+ instance.identifiers.length
160
+ # => 2
161
+
162
+ lccn = instance.identifiers.find { |id| id.types.include?("Lccn") }
163
+ isbn = instance.identifiers.find { |id| id.types.include?("Isbn") }
164
+ ```
165
+
166
+ ### Hash-Style Property Access
167
+
168
+ Any property can be accessed by name, even if there is no named accessor:
169
+
170
+ ```ruby
171
+ work["language"]
172
+ # => "http://id.loc.gov/vocabulary/languages/eng"
173
+
174
+ work["aap"]
175
+ # => "Dinniman, Matt. The dungeon anarchist's cookbook"
176
+ ```
177
+
178
+ ### Combining Multiple Documents
179
+
180
+ To parse related documents together (e.g., a Work and its Instance), combine their RDF graphs before building:
181
+
182
+ ```ruby
183
+ work_graph = BibframeRuby::Parser.new(work_json, format: :jsonld).parse
184
+ instance_graph = BibframeRuby::Parser.new(instance_json, format: :jsonld).parse
185
+
186
+ # Merge statements into one graph
187
+ instance_graph.each_statement { |s| work_graph << s }
188
+
189
+ # Build with linked relationships
190
+ result = BibframeRuby::Graph.from_rdf(work_graph)
191
+
192
+ result.works.first.instances.first.extent
193
+ # => "532 pages"
194
+ ```
195
+
196
+ ### Stub Resources
197
+
198
+ When a parsed document references an external resource by URI (e.g., an agent in the Library of Congress), a stub `Resource` is created with just the `id` set:
199
+
200
+ ```ruby
201
+ agent = work.contributions.first.agent
202
+ agent.id
203
+ # => "http://id.loc.gov/rwo/agents/no2023085548"
204
+
205
+ agent.is_a?(BibframeRuby::Resource)
206
+ # => true
207
+ ```
208
+
209
+ ## Model Reference
210
+
211
+ | Class | Accessors |
212
+ |-------|-----------|
213
+ | `Resource` | `id`, `types`, `properties`, `[]`, `[]=` |
214
+ | `Work` | `title`, `contributions`, `instances`, `language`, `subjects`, `genre_forms`, `summary`, `classifications`, `relations` |
215
+ | `Instance` | `title`, `work`, `identifiers`, `extent`, `carrier`, `media`, `provision_activity`, `edition_statement`, `dimensions`, `publication_statement`, `items` |
216
+ | `Hub` | `title`, `contributions`, `language`, `identifiers`, `relations`, `label` |
217
+ | `Item` | `instance`, `held_by`, `shelf_mark` |
218
+ | `Contribution` | `agent`, `role`, `primary?` |
219
+ | `Title` | `main_title`, `subtitle`, `non_sort_num` |
220
+ | `Agent` | `label` |
221
+ | `Person` | `label` (inherits from Agent) |
222
+ | `Organization` | `label` (inherits from Agent) |
223
+ | `Subject` | `label`, `source` |
224
+
225
+ All models inherit from `Resource` and support hash-style access via `[]` for any property.
226
+
227
+ ## Supported Formats
228
+
229
+ | Format | Status | File Extension |
230
+ |--------|--------|----------------|
231
+ | JSON-LD | Supported | `.jsonld` |
232
+ | Turtle | Planned | `.ttl` |
233
+ | RDF/XML | Planned | `.rdf` |
234
+
235
+ ## Development
236
+
237
+ After checking out the repo, run `bin/setup` to install dependencies.
238
+
239
+ ### Running Tests
240
+
241
+ ```bash
242
+ bundle exec rspec
243
+ ```
244
+
245
+ With documentation output:
246
+
247
+ ```bash
248
+ bundle exec rspec --format documentation
249
+ ```
250
+
251
+ ### Console
252
+
253
+ You can run `bin/console` for an interactive prompt to experiment with the gem.
254
+
255
+ ## Requirements
256
+
257
+ - Ruby >= 3.2.0
258
+
259
+ ## Contributing
260
+
261
+ Bug reports and pull requests are welcome on GitHub at https://github.com/aaron-collier/bibframe_ruby.
data/Rakefile ADDED
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "bundler/gem_tasks"
4
+ require "rspec/core/rake_task"
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ require "rubocop/rake_task"
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[spec rubocop]
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Graph
5
+ attr_reader :works, :instances, :items, :hubs, :resources
6
+
7
+ def initialize(works:, instances:, items:, resources:, hubs: [])
8
+ @works = works
9
+ @instances = instances
10
+ @items = items
11
+ @hubs = hubs
12
+ @resources = resources
13
+ end
14
+
15
+ def self.from_rdf(rdf_graph)
16
+ result = GraphBuilder.new(rdf_graph).build
17
+ new(
18
+ works: result[:works],
19
+ instances: result[:instances],
20
+ items: result[:items],
21
+ hubs: result[:hubs],
22
+ resources: result[:resources].values
23
+ )
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,270 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class GraphBuilder
5
+ BF = "http://id.loc.gov/ontologies/bibframe/"
6
+ BFLC = "http://id.loc.gov/ontologies/bflc/"
7
+ RDF_TYPE = RDF.type.to_s
8
+ RDFS_LABEL = RDF::RDFS.label.to_s
9
+
10
+ TYPE_MAP = {
11
+ "#{BF}Hub" => Hub,
12
+ "#{BF}Work" => Work,
13
+ "#{BF}Instance" => Instance,
14
+ "#{BF}Item" => Item,
15
+ "#{BF}Contribution" => Contribution,
16
+ "#{BF}PrimaryContribution" => Contribution,
17
+ "#{BF}Title" => Title,
18
+ "#{BF}Person" => Person,
19
+ "#{BF}Organization" => Organization,
20
+ "#{BF}Subject" => Subject
21
+ }.freeze
22
+
23
+ # Maps BIBFRAME predicates to Ruby property names
24
+ PROPERTY_MAP = {
25
+ "#{BF}title" => "title",
26
+ "#{BF}mainTitle" => "main_title",
27
+ "#{BF}subtitle" => "subtitle",
28
+ "#{BFLC}nonSortNum" => "non_sort_num",
29
+ "#{BF}contribution" => "contributions",
30
+ "#{BF}hasInstance" => "instances",
31
+ "#{BF}instanceOf" => "work",
32
+ "#{BF}hasItem" => "items",
33
+ "#{BF}itemOf" => "instance",
34
+ "#{BF}language" => "language",
35
+ "#{BF}summary" => "summary",
36
+ "#{BF}genreForm" => "genre_forms",
37
+ "#{BF}classification" => "classifications",
38
+ "#{BF}relation" => "relations",
39
+ "#{BF}identifiedBy" => "identifiers",
40
+ "#{BF}extent" => "extent",
41
+ "#{BF}carrier" => "carrier",
42
+ "#{BF}media" => "media",
43
+ "#{BF}provisionActivity" => "provision_activity",
44
+ "#{BF}editionStatement" => "edition_statement",
45
+ "#{BF}dimensions" => "dimensions",
46
+ "#{BF}publicationStatement" => "publication_statement",
47
+ "#{BF}responsibilityStatement" => "responsibility_statement",
48
+ "#{BF}issuance" => "issuance",
49
+ "#{BF}agent" => "agent",
50
+ "#{BF}role" => "role",
51
+ "#{BF}heldBy" => "held_by",
52
+ "#{BF}shelfMark" => "shelf_mark",
53
+ "#{BF}content" => "content",
54
+ "#{BF}subject" => "subjects",
55
+ RDFS_LABEL => "label",
56
+ "#{BF}code" => "code",
57
+ "#{BF}source" => "source",
58
+ "#{BF}status" => "status",
59
+ "#{BF}date" => "date",
60
+ "#{BF}seriesEnumeration" => "series_enumeration",
61
+ "#{BF}associatedResource" => "associated_resource",
62
+ "#{BF}relationship" => "relationship",
63
+ "#{BF}adminMetadata" => "admin_metadata",
64
+ "#{BF}derivedFrom" => "derived_from",
65
+ "#{BF}assigner" => "assigner",
66
+ "#{BF}classificationPortion" => "classification_portion",
67
+ "#{BF}itemPortion" => "item_portion",
68
+ "#{BF}edition" => "edition",
69
+ "#{BF}descriptionLevel" => "description_level",
70
+ "#{BF}descriptionLanguage" => "description_language",
71
+ "#{BF}descriptionAuthentication" => "description_authentication",
72
+ "#{BFLC}catalogerId" => "cataloger_id",
73
+ "#{BFLC}aap" => "Authorized Access Point",
74
+ "#{BFLC}aap-normalized" => "aap_normalized",
75
+ "#{BFLC}simpleDate" => "simple_date",
76
+ "#{BFLC}simpleAgent" => "simple_agent",
77
+ "#{BFLC}simplePlace" => "simple_place"
78
+ }.freeze
79
+
80
+ # Properties that always hold arrays
81
+ ARRAY_PROPERTIES = %w[
82
+ contributions instances items genre_forms classifications
83
+ relations identifiers subjects admin_metadata
84
+ ].freeze
85
+
86
+ # Properties whose blank-node value should be collapsed to its rdfs:label string
87
+ LABEL_COLLAPSE_PROPERTIES = %w[summary extent].freeze
88
+
89
+ def initialize(rdf_graph)
90
+ @rdf_graph = rdf_graph
91
+ @resources = {}
92
+ @stubs = {}
93
+ end
94
+
95
+ def build
96
+ grouped = group_by_subject
97
+ extract_resources(grouped)
98
+ link_relationships
99
+
100
+ {
101
+ resources: @resources,
102
+ works: @resources.values.select { |r| r.is_a?(Work) },
103
+ instances: @resources.values.select { |r| r.is_a?(Instance) },
104
+ items: @resources.values.select { |r| r.is_a?(Item) },
105
+ hubs: @resources.values.select { |r| r.is_a?(Hub) }
106
+ }
107
+ end
108
+
109
+ private
110
+
111
+ def group_by_subject
112
+ groups = Hash.new { |h, k| h[k] = [] }
113
+ @rdf_graph.each_statement { |s| groups[s.subject] << s }
114
+ groups
115
+ end
116
+
117
+ def extract_resources(grouped)
118
+ grouped.each do |subject, statements|
119
+ # Skip blank nodes that will be resolved inline as sub-resources
120
+ next if subject.is_a?(RDF::Node)
121
+
122
+ types = extract_types(statements)
123
+ klass = resolve_class(types)
124
+ type_names = types.map { |t| t.to_s.split(%r{[/#]}).last }
125
+
126
+ resource = klass.new(
127
+ id: subject.to_s,
128
+ types: type_names
129
+ )
130
+
131
+ populate_properties(resource, statements, grouped)
132
+ register_resource(subject, resource)
133
+ end
134
+ end
135
+
136
+ def extract_types(statements)
137
+ statements
138
+ .select { |s| s.predicate.to_s == RDF_TYPE }
139
+ .map(&:object)
140
+ end
141
+
142
+ def resolve_class(types)
143
+ type_strings = types.map(&:to_s)
144
+ TYPE_MAP.each do |type_uri, klass|
145
+ return klass if type_strings.include?(type_uri)
146
+ end
147
+ Resource
148
+ end
149
+
150
+ def populate_properties(resource, statements, grouped)
151
+ statements.each do |stmt|
152
+ next if stmt.predicate.to_s == RDF_TYPE
153
+
154
+ prop_name = PROPERTY_MAP[stmt.predicate.to_s] || stmt.predicate.to_s.split(%r{[/#]}).last
155
+ value = resolve_value(stmt.object, grouped, prop_name)
156
+ next if value.nil?
157
+
158
+ if ARRAY_PROPERTIES.include?(prop_name)
159
+ resource[prop_name] ||= []
160
+ resource[prop_name] << value
161
+ elsif resource[prop_name].nil?
162
+ resource[prop_name] = value
163
+ elsif resource[prop_name].is_a?(Array)
164
+ resource[prop_name] << value
165
+ else
166
+ resource[prop_name] = [resource[prop_name], value]
167
+ end
168
+ end
169
+
170
+ # Mark contribution as primary if PrimaryContribution is in its types
171
+ return unless resource.is_a?(Contribution)
172
+
173
+ types_uris = statements
174
+ .select { |s| s.predicate.to_s == RDF_TYPE }
175
+ .map { |s| s.object.to_s }
176
+ resource["primary"] = true if types_uris.include?("#{BF}PrimaryContribution")
177
+ end
178
+
179
+ def resolve_value(object, grouped, prop_name = nil)
180
+ case object
181
+ when RDF::Node
182
+ resolve_blank_node(object, grouped, prop_name)
183
+ when RDF::URI
184
+ object.to_s
185
+ when RDF::Literal
186
+ object.object.to_s
187
+ else
188
+ object.to_s
189
+ end
190
+ end
191
+
192
+ def resolve_blank_node(node, grouped, prop_name = nil)
193
+ return nil unless grouped.key?(node)
194
+
195
+ sub_statements = grouped[node]
196
+ types = extract_types(sub_statements)
197
+ klass = resolve_class(types)
198
+ type_names = types.map { |t| t.to_s.split(%r{[/#]}).last }
199
+
200
+ sub_resource = klass.new(types: type_names)
201
+ populate_properties(sub_resource, sub_statements, grouped)
202
+ register_resource(node, sub_resource)
203
+
204
+ # Collapse label-only resources to their string label
205
+ return sub_resource["label"] if LABEL_COLLAPSE_PROPERTIES.include?(prop_name) && sub_resource["label"]
206
+
207
+ sub_resource
208
+ end
209
+
210
+ def register_resource(subject, resource)
211
+ key = subject.to_s
212
+ @resources[key] = resource
213
+ end
214
+
215
+ def link_relationships
216
+ link_work_instances
217
+ link_instance_items
218
+ replace_uri_stubs
219
+ end
220
+
221
+ def link_work_instances
222
+ @resources.values.select { |r| r.is_a?(Work) }.each do |work|
223
+ next unless work["instances"]
224
+
225
+ work["instances"] = work["instances"].map do |ref|
226
+ uri = ref.is_a?(String) ? ref : ref.id
227
+ @resources[uri] || stub_for(uri)
228
+ end
229
+ end
230
+
231
+ @resources.values.select { |r| r.is_a?(Instance) }.each do |instance|
232
+ next unless instance["work"]
233
+
234
+ uri = instance["work"].is_a?(String) ? instance["work"] : instance["work"].id
235
+ instance["work"] = @resources[uri] || stub_for(uri)
236
+ end
237
+ end
238
+
239
+ def link_instance_items
240
+ @resources.values.select { |r| r.is_a?(Instance) }.each do |instance|
241
+ next unless instance["items"]
242
+
243
+ instance["items"] = instance["items"].map do |ref|
244
+ uri = ref.is_a?(String) ? ref : ref.id
245
+ @resources[uri] || stub_for(uri)
246
+ end
247
+ end
248
+
249
+ @resources.values.select { |r| r.is_a?(Item) }.each do |item|
250
+ next unless item["instance"]
251
+
252
+ uri = item["instance"].is_a?(String) ? item["instance"] : item["instance"].id
253
+ item["instance"] = @resources[uri] || stub_for(uri)
254
+ end
255
+ end
256
+
257
+ def replace_uri_stubs
258
+ # Replace any remaining string URI references in contribution agents
259
+ @resources.values.select { |r| r.is_a?(Contribution) }.each do |contrib|
260
+ next unless contrib["agent"].is_a?(String)
261
+
262
+ contrib["agent"] = @resources[contrib["agent"]] || stub_for(contrib["agent"])
263
+ end
264
+ end
265
+
266
+ def stub_for(uri)
267
+ @stubs[uri] ||= Resource.new(id: uri)
268
+ end
269
+ end
270
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Agent < Resource
5
+ def label
6
+ self["label"]
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Contribution < Resource
5
+ def agent
6
+ self["agent"]
7
+ end
8
+
9
+ def role
10
+ self["role"]
11
+ end
12
+
13
+ def primary?
14
+ self["primary"] == true
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Hub < Resource
5
+ def title
6
+ self["title"]
7
+ end
8
+
9
+ def contributions
10
+ self["contributions"] || []
11
+ end
12
+
13
+ def language
14
+ self["language"]
15
+ end
16
+
17
+ def identifiers
18
+ self["identifiers"] || []
19
+ end
20
+
21
+ def relations
22
+ self["relations"] || []
23
+ end
24
+
25
+ def label
26
+ self["label"]
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Instance < Resource
5
+ def title
6
+ self["title"]
7
+ end
8
+
9
+ def work
10
+ self["work"]
11
+ end
12
+
13
+ def identifiers
14
+ self["identifiers"] || []
15
+ end
16
+
17
+ def extent
18
+ self["extent"]
19
+ end
20
+
21
+ def carrier
22
+ self["carrier"]
23
+ end
24
+
25
+ def media
26
+ self["media"]
27
+ end
28
+
29
+ def provision_activity
30
+ self["provision_activity"]
31
+ end
32
+
33
+ def edition_statement
34
+ self["edition_statement"]
35
+ end
36
+
37
+ def dimensions
38
+ self["dimensions"]
39
+ end
40
+
41
+ def publication_statement
42
+ self["publication_statement"]
43
+ end
44
+
45
+ def items
46
+ self["items"] || []
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Item < Resource
5
+ def instance
6
+ self["instance"]
7
+ end
8
+
9
+ def held_by
10
+ self["held_by"]
11
+ end
12
+
13
+ def shelf_mark
14
+ self["shelf_mark"]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Organization < Agent
5
+ end
6
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Person < Agent
5
+ end
6
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Subject < Resource
5
+ def label
6
+ self["label"]
7
+ end
8
+
9
+ def source
10
+ self["source"]
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Title < Resource
5
+ def main_title
6
+ self["main_title"]
7
+ end
8
+
9
+ def subtitle
10
+ self["subtitle"]
11
+ end
12
+
13
+ def non_sort_num
14
+ self["non_sort_num"]
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Work < Resource
5
+ def title
6
+ self["title"]
7
+ end
8
+
9
+ def contributions
10
+ self["contributions"] || []
11
+ end
12
+
13
+ def instances
14
+ self["instances"] || []
15
+ end
16
+
17
+ def language
18
+ self["language"]
19
+ end
20
+
21
+ def subjects
22
+ self["subjects"] || []
23
+ end
24
+
25
+ def genre_forms
26
+ self["genre_forms"] || []
27
+ end
28
+
29
+ def summary
30
+ self["summary"]
31
+ end
32
+
33
+ def classifications
34
+ self["classifications"] || []
35
+ end
36
+
37
+ def relations
38
+ self["relations"] || []
39
+ end
40
+
41
+ def aap
42
+ self["aap"]
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rdf"
4
+ require "json/ld"
5
+ require "rdf/turtle"
6
+
7
+ module BibframeRuby
8
+ class Parser
9
+ EXTENSION_MAP = {
10
+ ".jsonld" => :jsonld,
11
+ ".ttl" => :turtle,
12
+ ".rdf" => :rdfxml
13
+ }.freeze
14
+
15
+ READER_MAP = {
16
+ jsonld: JSON::LD::Reader,
17
+ turtle: RDF::Turtle::Reader
18
+ }.freeze
19
+
20
+ def initialize(input, format: :jsonld)
21
+ @input = input
22
+ @format = format
23
+ end
24
+
25
+ def parse
26
+ reader_class = READER_MAP.fetch(@format) do
27
+ raise BibframeRuby::Error, "Unsupported format: #{@format}"
28
+ end
29
+
30
+ graph = RDF::Graph.new
31
+ reader_class.new(@input) { |reader| graph << reader }
32
+ graph
33
+ end
34
+
35
+ def self.format_for_extension(ext)
36
+ EXTENSION_MAP.fetch(ext) do
37
+ raise BibframeRuby::Error, "Unknown file extension: #{ext}"
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ class Resource
5
+ attr_reader :id, :types
6
+ attr_accessor :properties
7
+
8
+ def initialize(id: nil, types: [], properties: {})
9
+ @id = id
10
+ @types = types
11
+ @properties = properties
12
+ end
13
+
14
+ def [](key)
15
+ @properties[key.to_s]
16
+ end
17
+
18
+ def []=(key, value)
19
+ @properties[key.to_s] = value
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module BibframeRuby
4
+ VERSION = "0.1.0"
5
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "net/http"
4
+ require "uri"
5
+ require_relative "bibframe_ruby/version"
6
+ require_relative "bibframe_ruby/resource"
7
+ require_relative "bibframe_ruby/models/title"
8
+ require_relative "bibframe_ruby/models/work"
9
+ require_relative "bibframe_ruby/models/instance"
10
+ require_relative "bibframe_ruby/models/item"
11
+ require_relative "bibframe_ruby/models/contribution"
12
+ require_relative "bibframe_ruby/models/agent"
13
+ require_relative "bibframe_ruby/models/person"
14
+ require_relative "bibframe_ruby/models/organization"
15
+ require_relative "bibframe_ruby/models/subject"
16
+ require_relative "bibframe_ruby/models/hub"
17
+ require_relative "bibframe_ruby/parser"
18
+ require_relative "bibframe_ruby/graph_builder"
19
+ require_relative "bibframe_ruby/graph"
20
+
21
+ module BibframeRuby
22
+ class Error < StandardError; end
23
+
24
+ def self.parse(input, format: :jsonld)
25
+ rdf_graph = Parser.new(input, format: format).parse
26
+ Graph.from_rdf(rdf_graph)
27
+ end
28
+
29
+ def self.parse_file(path)
30
+ ext = File.extname(path)
31
+ format = Parser.format_for_extension(ext)
32
+ input = File.read(path)
33
+ parse(input, format: format)
34
+ end
35
+
36
+ def self.parse_uri(uri, redirect_limit: 5)
37
+ raise Error, "Too many redirects" if redirect_limit.zero?
38
+
39
+ parsed_uri = URI.parse(uri)
40
+ response = Net::HTTP.get_response(parsed_uri)
41
+
42
+ if response.is_a?(Net::HTTPSuccess)
43
+ ext = File.extname(parsed_uri.path)
44
+ format = ext.empty? ? :jsonld : Parser.format_for_extension(ext)
45
+ parse(response.body, format: format)
46
+ elsif response.is_a?(Net::HTTPRedirection)
47
+ parse_uri(response["location"], redirect_limit: redirect_limit - 1)
48
+ else
49
+ raise Error, "HTTP error: #{response.code} #{response.message}"
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,4 @@
1
+ module BibframeRuby
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,116 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: bibframe_ruby
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Aaron Collier
8
+ bindir: exe
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: json-ld
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - ">="
17
+ - !ruby/object:Gem::Version
18
+ version: '0'
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - ">="
24
+ - !ruby/object:Gem::Version
25
+ version: '0'
26
+ - !ruby/object:Gem::Dependency
27
+ name: rdf
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: '0'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: rdf-rdfxml
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
54
+ - !ruby/object:Gem::Dependency
55
+ name: rdf-turtle
56
+ requirement: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - ">="
59
+ - !ruby/object:Gem::Version
60
+ version: '0'
61
+ type: :runtime
62
+ prerelease: false
63
+ version_requirements: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - ">="
66
+ - !ruby/object:Gem::Version
67
+ version: '0'
68
+ description: Ruby gem for parsing various BIBFRAME supported formats for display.
69
+ email:
70
+ - aaron.collier@stanford.edu
71
+ executables: []
72
+ extensions: []
73
+ extra_rdoc_files: []
74
+ files:
75
+ - README.md
76
+ - Rakefile
77
+ - lib/bibframe_ruby.rb
78
+ - lib/bibframe_ruby/graph.rb
79
+ - lib/bibframe_ruby/graph_builder.rb
80
+ - lib/bibframe_ruby/models/agent.rb
81
+ - lib/bibframe_ruby/models/contribution.rb
82
+ - lib/bibframe_ruby/models/hub.rb
83
+ - lib/bibframe_ruby/models/instance.rb
84
+ - lib/bibframe_ruby/models/item.rb
85
+ - lib/bibframe_ruby/models/organization.rb
86
+ - lib/bibframe_ruby/models/person.rb
87
+ - lib/bibframe_ruby/models/subject.rb
88
+ - lib/bibframe_ruby/models/title.rb
89
+ - lib/bibframe_ruby/models/work.rb
90
+ - lib/bibframe_ruby/parser.rb
91
+ - lib/bibframe_ruby/resource.rb
92
+ - lib/bibframe_ruby/version.rb
93
+ - sig/bibframe_ruby.rbs
94
+ homepage: https://github.com/aaron-collier/bibframe_ruby
95
+ licenses: []
96
+ metadata:
97
+ homepage_uri: https://github.com/aaron-collier/bibframe_ruby
98
+ source_code_uri: https://github.com/aaron-collier/bibframe_ruby
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: 3.2.0
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - ">="
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ requirements: []
113
+ rubygems_version: 3.6.9
114
+ specification_version: 4
115
+ summary: Ruby gem for parsing various BIBFRAME supported formats for display.
116
+ test_files: []