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 +7 -0
- data/README.md +261 -0
- data/Rakefile +12 -0
- data/lib/bibframe_ruby/graph.rb +26 -0
- data/lib/bibframe_ruby/graph_builder.rb +270 -0
- data/lib/bibframe_ruby/models/agent.rb +9 -0
- data/lib/bibframe_ruby/models/contribution.rb +17 -0
- data/lib/bibframe_ruby/models/hub.rb +29 -0
- data/lib/bibframe_ruby/models/instance.rb +49 -0
- data/lib/bibframe_ruby/models/item.rb +17 -0
- data/lib/bibframe_ruby/models/organization.rb +6 -0
- data/lib/bibframe_ruby/models/person.rb +6 -0
- data/lib/bibframe_ruby/models/subject.rb +13 -0
- data/lib/bibframe_ruby/models/title.rb +17 -0
- data/lib/bibframe_ruby/models/work.rb +45 -0
- data/lib/bibframe_ruby/parser.rb +41 -0
- data/lib/bibframe_ruby/resource.rb +22 -0
- data/lib/bibframe_ruby/version.rb +5 -0
- data/lib/bibframe_ruby.rb +52 -0
- data/sig/bibframe_ruby.rbs +4 -0
- metadata +116 -0
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,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,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,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,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
|
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: []
|