cocina_display 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: 1e3637cdbaae1a3227f2cfac3186ea15648151c0d90e5d71f678d32bb4242487
4
+ data.tar.gz: 12ee8a16fc75ff83361bbc929d3d15693f9dcf121585d61654c8cd5da60ca2d8
5
+ SHA512:
6
+ metadata.gz: 02cea2ac8bbb9ecab1d142f48e78d237762d6394615f87aed6a9aa535f2d34e2bea6b3d4629e0851853e9b8708d2578d26c9e1baec8a0a7c952c7c1d8deffc4a
7
+ data.tar.gz: ba18d7424b7114525a5100bf73b619e9dbae259e2ec35fb6f0a754981457d8c67b6f686c3a4d53ef98f29122291fd564e6768fb2a348744e3e54813df3b03049
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.standard.yml ADDED
@@ -0,0 +1,3 @@
1
+ # For available configuration options, see:
2
+ # https://github.com/standardrb/standard
3
+ ruby_version: 3.1
data/LICENSE ADDED
@@ -0,0 +1,13 @@
1
+ Copyright 2025 The Board of Trustees of the Leland Stanford Junior University.
2
+
3
+ Licensed under the Apache License, Version 2.0 (the "License");
4
+ you may not use this file except in compliance with the License.
5
+ You may obtain a copy of the License at
6
+
7
+ http://www.apache.org/licenses/LICENSE-2.0
8
+
9
+ Unless required by applicable law or agreed to in writing, software
10
+ distributed under the License is distributed on an "AS IS" BASIS,
11
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ See the License for the specific language governing permissions and
13
+ limitations under the License.
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ # CocinaDisplay
2
+
3
+ [![Build Status](https://github.com/sul-dlss/cocina_display/workflows/CI/badge.svg)](https://github.com/sul-dlss/cocina_display/actions)
4
+ [![Docs Status](https://github.com/sul-dlss/cocina_display/actions/workflows/docs.yml/badge.svg)](https://github.com/sul-dlss/cocina_display/actions/workflows/docs.yml)
5
+ [![Gem Version](https://badge.fury.io/rb/cocina_display.svg)](https://badge.fury.io/rb/cocina_display)
6
+
7
+ Helpers for rendering Cocina metadata in Rails applications and indexing pipelines.
8
+
9
+ ## Installation
10
+
11
+ Install the gem and add to the application's Gemfile by executing:
12
+
13
+ ```bash
14
+ bundle add cocina_display
15
+ ```
16
+
17
+ If bundler is not being used to manage dependencies, install the gem by executing:
18
+
19
+ ```bash
20
+ gem install cocina_display
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ### Obtaining Cocina
26
+
27
+ To start, you need some Cocina in JSON form.
28
+
29
+ You can download some directly from PURL by visiting an object's PURL URL and appending `.json` to the end, like `https://purl.stanford.edu/bb112zx3193.json`. Some examples are available in the `spec/fixtures` directory.
30
+
31
+ You can also use the built-in `HTTP` library or `faraday` gem to fetch the record for you, e.g.:
32
+
33
+ ```ruby
34
+ require 'http'
35
+ cocina_json = HTTP.get('https://purl.stanford.edu/bb112zx3193.json').to_s
36
+ ```
37
+
38
+ ### Working with objects
39
+
40
+ Once you have the JSON, you can initialize a `CocinaRecord` object and start working with it. The `CocinaRecord` class provides some methods to access common fields, as well as an underlying hash representation parsed from the JSON.
41
+
42
+ ```ruby
43
+ > require 'cocina_display/cocina_record'
44
+ => true
45
+ > record = CocinaDisplay::CocinaRecord.new(cocina_json)
46
+ =>
47
+ #<CocinaDisplay::CocinaRecord:0x000000012d11b600
48
+ ...
49
+ > record.titles
50
+ => ["Bugatti Type 51A. Road & Track Salon January 1957"]
51
+ > record.content_type
52
+ => "image"
53
+ > record.iiif_manifest_url
54
+ => "https://purl.stanford.edu/bb112zx3193/iiif3/manifest"
55
+ > record.cocina_doc.dig("description", "contributor", 0, "name", 0, "value") # access the hash representation
56
+ => "Hearst Magazines, Inc."
57
+ ```
58
+
59
+ See the [API Documentation](https://sul-dlss.github.io/cocina_display/CocinaDisplay/CocinaRecord.html) for more details on the methods available in the `CocinaRecord` class.
60
+
61
+ ### Fetching nested data
62
+
63
+ Fetching data deeply nested in the record, especially when you need to filter based on some criteria, can be tedious. The `CocinaRecord` class also provides a method called `#path` that accepts a JsonPath expression to retrieve data in a more concise way.
64
+
65
+ The previous example used `Hash#dig` to access the first contributor's first name value. Using `#path`, you can query for _all_ contributor name values, or even filter to particular contributors:
66
+
67
+ ```ruby
68
+ > record.path('$.description.contributor[*].name[*].value').search # name values for all contributors in description
69
+ => ["Hearst Magazines, Inc.", "Chesebrough, Jerry"]
70
+ > record.path("$.description.contributor[?@.role[?@.value == 'photographer']].name[*].value").search # only contributors with a role with value "photographer"
71
+ => ["Chesebrough, Jerry"]
72
+ >
73
+ ```
74
+
75
+ The JsonPath implementation used is [`janeway-jsonpath`](https://www.rubydoc.info/gems/janeway-jsonpath/0.6.0/file/README.md), which supports the full syntax from the [finalized 2024 version of the specification](https://www.rfc-editor.org/rfc/rfc9535.html). Results returned from `#path` are Enumerators.
76
+
77
+ In the following example, we start an expression with `"$.."` to search for contributor nodes at _any_ level (e.g. `event.contributors`) and discover that there is a third contributor, but it has no `name` value. Using the `['code', 'value']` syntax, we can retrieve both `code` and `value` and show where they came from:
78
+
79
+ ```ruby
80
+ > record.path("$..contributor[*].name[*]['code', 'value']").each { |value, node, key| puts "#{key}: #{value} (from #{node})" }
81
+ value: Hearst Magazines, Inc. (from {"structuredValue"=>[], "parallelValue"=>[], "groupedValue"=>[], "value"=>"Hearst Magazines, Inc.", "uri"=>"http://id.loc.gov/authorities/names/n2015050736", "identifier"=>[], "source"=>{"code"=>"naf", "uri"=>"http://id.loc.gov/authorities/names/", "note"=>[]}, "note"=>[], "appliesTo"=>[]})
82
+ value: Chesebrough, Jerry (from {"structuredValue"=>[], "parallelValue"=>[], "groupedValue"=>[], "value"=>"Chesebrough, Jerry", "identifier"=>[], "note"=>[], "appliesTo"=>[]})
83
+ code: CSt (from {"structuredValue"=>[], "parallelValue"=>[], "groupedValue"=>[], "code"=>"CSt", "uri"=>"http://id.loc.gov/vocabulary/organizations/cst", "identifier"=>[], "source"=>{"code"=>"marcorg", "uri"=>"http://id.loc.gov/vocabulary/organizations", "note"=>[]}, "note"=>[], "appliesTo"=>[]})
84
+ => ["Hearst Magazines, Inc.", "Chesebrough, Jerry", "CSt"]
85
+ ```
86
+
87
+ There is also a command line utility for quickly querying a JSON file using JsonPath. Online syntax checkers may give different results, so it helps to test locally. You can run it with:
88
+
89
+ ```bash
90
+ cat spec/fixtures/bb112zx3193.json | janeway "$.description.contributor[?@.role[?@.value == 'photographer']].name[*].value"
91
+ [
92
+ "Chesebrough, Jerry"
93
+ ]
94
+ ```
95
+
96
+ ## Development
97
+
98
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
99
+
100
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
101
+
102
+ Documentation is generated using [yard](https://yardoc.org). You can generate it by running `yardoc`, or `yard server --reload` to start a local server and watch for changes as you edit.
103
+
104
+ ## Background
105
+
106
+ Historically, applications at SUL used a combination of several gems to render objects represented by MODS XML. With the transition to the Cocina data model, infrastructure applications adopted the [`cocina-models` gem](https://github.com/sul-dlss/cocina-models), which provides accessor objects and validators over Cocina JSON. Internal applications can fetch such objects over HTTP using [`dor-services-client`](https://github.com/sul-dlss/dor-services-client).
107
+
108
+ On the access side, Cocina JSON (the "public Cocina") is available statically via [PURL](https://purl.stanford.edu), but is only updated when an object is published ("shelved") from SDR. This frequently results in data that is technically invalid with respect to `cocina-models` but is still valid in the context of a patron-facing application.
109
+
110
+ Cocina data can also be complex, representing the same underlying information in different ways. A "complete" implementation can involve checking multiple deeply nested paths to ensure no information is missed. Rather than tightly coupling access applications to `cocina-models`, this gem provides a set of helpers designed to safely parse Cocina JSON and render it in a consistent way across applications.
111
+
112
+ The intent is that both applications that directly render SDR metadata as HTML (PURL, Exhibits) and applications that index it for later display in a catalog (Searchworks, Earthworks, Dataworks) can adopt a single gem for rendering Cocina in a human-readable way. This gem **does not** aim to render HTML or provide view components – that is the responsibility of the consuming application.
data/Rakefile ADDED
@@ -0,0 +1,10 @@
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 "standard/rake"
9
+
10
+ task default: %i[spec standard]
@@ -0,0 +1,189 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cocina/models"
4
+ require "janeway"
5
+
6
+ module CocinaDisplay
7
+ # Public Cocina metadata for an SDR object
8
+ class CocinaRecord
9
+ # The parsed Cocina document.
10
+ # @return [Hash]
11
+ attr_reader :cocina_doc
12
+
13
+ def initialize(cocina_json)
14
+ @cocina_doc = JSON.parse(cocina_json)
15
+ end
16
+
17
+ # Evaluate a JSONPath expression against the Cocina document.
18
+ # @return [Enumerator] An enumerator that yields results matching the expression.
19
+ # @param path_expression [String] The JSONPath expression to evaluate.
20
+ # @see https://www.rubydoc.info/gems/janeway-jsonpath/0.6.0/file/README.md
21
+ # @example Name values for contributors
22
+ # record.path("$.description.contributor[*].name[*].value").search #=> ["Smith, John", "ACME Corp."]
23
+ # @example Filtering nodes using a condition
24
+ # record.path("$.description.contributor[?(@.type == 'person')].name[*].value").search #=> ["Smith, John"]
25
+ def path(path_expression)
26
+ Janeway.enum_for(path_expression, cocina_doc)
27
+ end
28
+
29
+ # The DRUID for the object, with the +druid:+ prefix.
30
+ # @return [String]
31
+ # @example
32
+ # record.druid #=> "druid:bb099mt5053"
33
+ def druid
34
+ cocina_doc["externalIdentifier"]
35
+ end
36
+
37
+ # The DRUID for the object, without the +druid:+ prefix.
38
+ # @return [String]
39
+ # @example
40
+ # record.bare_druid #=> "bb099mt5053"
41
+ def bare_druid
42
+ druid.delete_prefix("druid:")
43
+ end
44
+
45
+ # The DOI for the object, if there is one – just the identifier part.
46
+ # @return [String, nil]
47
+ # @example
48
+ # record.doi #=> "10.25740/ppax-bf07"
49
+ def doi
50
+ doi_id = path("$.identification.doi").first ||
51
+ path("$.description.identifier[?match(@.type, 'doi|DOI')].value").first ||
52
+ path("$.description.identifier[?search(@.uri, 'doi.org')].uri").first
53
+
54
+ URI(doi_id).path.delete_prefix("/") if doi_id.present?
55
+ end
56
+
57
+ # The DOI as a URL, if there is one. Any valid DOI should resolve via doi.org.
58
+ # @return [String, nil]
59
+ # @example
60
+ # record.doi_url #=> "https://doi.org/10.25740/ppax-bf07"
61
+ def doi_url
62
+ URI.join("https://doi.org", doi).to_s if doi.present?
63
+ end
64
+
65
+ # The HRID of the item in FOLIO, if defined.
66
+ # @note This doesn't imply the object is available in Searchworks at this ID.
67
+ # @return [String, nil]
68
+ # @example
69
+ # record.folio_hrid #=> "a12845814"
70
+ def folio_hrid
71
+ path("$.identification.catalogLinks[?(@.catalog == 'folio')].catalogRecordId").first
72
+ end
73
+
74
+ # The FOLIO HRID if defined, otherwise the bare DRUID.
75
+ # @note This doesn't imply the object is available in Searchworks at this ID.
76
+ # @see folio_hrid
77
+ # @see bare_druid
78
+ # @return [String]
79
+ def searchworks_id
80
+ folio_hrid || bare_druid
81
+ end
82
+
83
+ # Timestamp when the Cocina was created.
84
+ # @note This is for the metadata itself, not the object.
85
+ # @return [Time]
86
+ def created_time
87
+ Time.parse(cocina_doc["created"])
88
+ end
89
+
90
+ # Timestamp when the Cocina was last modified.
91
+ # @note This is for the metadata itself, not the object.
92
+ # @return [Time]
93
+ def modified_time
94
+ Time.parse(cocina_doc["modified"])
95
+ end
96
+
97
+ # SDR content type of the object.
98
+ # @return [String]
99
+ # @see https://github.com/sul-dlss/cocina-models/blob/main/openapi.yml#L532-L546
100
+ # @example
101
+ # record.content_type #=> "image"
102
+ def content_type
103
+ cocina_doc["type"].split("/").last
104
+ end
105
+
106
+ # True if the object is a collection.
107
+ # @return [Boolean]
108
+ def collection?
109
+ content_type == "collection"
110
+ end
111
+
112
+ # Traverse nested FileSets and return an enumerator over their files.
113
+ # Each file is a +Hash+.
114
+ # @return [Enumerator] Enumerator over file hashes
115
+ # @example
116
+ # record.files.each do |file|
117
+ # puts file["filename"] #=> "image1.jpg"
118
+ # puts file["size"] #=> 123456
119
+ # end
120
+ def files
121
+ path("$.structural.contains[*].structural.contains[*]")
122
+ end
123
+
124
+ # The PURL URL for this object.
125
+ # @return [String]
126
+ # @example
127
+ # record.purl_url #=> "https://purl.stanford.edu/bx658jh7339"
128
+ def purl_url
129
+ cocina_doc.dig("description", "purl") || "https://purl.stanford.edu/#{bare_druid}"
130
+ end
131
+
132
+ # The URL to the PURL environment this object is from.
133
+ # @note Objects accessed via UAT will still have a production PURL base URL.
134
+ # @return [String]
135
+ # @example
136
+ # record.purl_base_url #=> "https://purl.stanford.edu"
137
+ def purl_base_url
138
+ URI(purl_url).origin
139
+ end
140
+
141
+ # The URL to the stacks environment this object is shelved in.
142
+ # Corresponds to the PURL environment.
143
+ # @see purl_base_url
144
+ # @return [String]
145
+ # @example
146
+ # record.stacks_base_url #=> "https://stacks.stanford.edu"
147
+ def stacks_base_url
148
+ if purl_base_url == "https://sul-purl-stage.stanford.edu"
149
+ "https://sul-stacks-stage.stanford.edu"
150
+ else
151
+ "https://stacks.stanford.edu"
152
+ end
153
+ end
154
+
155
+ # The oEmbed URL for the object, optionally with additional parameters.
156
+ # Corresponds to the PURL environment.
157
+ # @param params [Hash] Additional parameters to include in the oEmbed URL.
158
+ # @return [String]
159
+ # @return [nil] if the object is a collection.
160
+ # @example Generate an oEmbed URL for the viewer and hide the title
161
+ # record.oembed_url(hide_title: true) #=> "https://purl.stanford.edu/bx658jh7339/embed.json?hide_title=true"
162
+ def oembed_url(params: {})
163
+ return if collection?
164
+
165
+ params[:url] ||= purl_url
166
+ "#{purl_base_url}/embed.json?#{params.to_query}"
167
+ end
168
+
169
+ # The download URL to get the entire object as a .zip file.
170
+ # Stacks generates the .zip for the object on request.
171
+ # @return [String]
172
+ # @example
173
+ # record.download_url #=> "https://stacks.stanford.edu/object/bx658jh7339"
174
+ def download_url
175
+ "#{stacks_base_url}/object/#{bare_druid}"
176
+ end
177
+
178
+ # The IIIF manifest URL for the object.
179
+ # PURL generates the IIIF manifest.
180
+ # @param version [Integer] The IIIF presentation spec version to use (3 or 2).
181
+ # @return [String]
182
+ # @example
183
+ # record.iiif_manifest_url #=> "https://purl.stanford.edu/bx658jh7339/iiif3/manifest"
184
+ def iiif_manifest_url(version: 3)
185
+ iiif_path = (version == 3) ? "iiif3" : "iiif"
186
+ "#{purl_url}/#{iiif_path}/manifest"
187
+ end
188
+ end
189
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ # :nodoc:
4
+ module CocinaDisplay
5
+ VERSION = "0.1.0" # :nodoc:
6
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "cocina_display/version"
4
+ require_relative "cocina_display/cocina_record"
@@ -0,0 +1,4 @@
1
+ module CocinaDisplay
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,152 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cocina_display
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Nick Budak
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2025-06-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: cocina-models
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0.101'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0.101'
27
+ - !ruby/object:Gem::Dependency
28
+ name: janeway-jsonpath
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '0.6'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '0.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '13.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '13.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: standard
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - "~>"
74
+ - !ruby/object:Gem::Version
75
+ version: '1.3'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - "~>"
81
+ - !ruby/object:Gem::Version
82
+ version: '1.3'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simplecov
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - "~>"
88
+ - !ruby/object:Gem::Version
89
+ version: 0.22.0
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - "~>"
95
+ - !ruby/object:Gem::Version
96
+ version: 0.22.0
97
+ - !ruby/object:Gem::Dependency
98
+ name: yard
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - "~>"
102
+ - !ruby/object:Gem::Version
103
+ version: 0.9.37
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - "~>"
109
+ - !ruby/object:Gem::Version
110
+ version: 0.9.37
111
+ description:
112
+ email:
113
+ - budak@stanford.edu
114
+ executables: []
115
+ extensions: []
116
+ extra_rdoc_files: []
117
+ files:
118
+ - ".rspec"
119
+ - ".standard.yml"
120
+ - LICENSE
121
+ - README.md
122
+ - Rakefile
123
+ - lib/cocina_display.rb
124
+ - lib/cocina_display/cocina_record.rb
125
+ - lib/cocina_display/version.rb
126
+ - sig/cocina_display.rbs
127
+ homepage: https://sul-dlss.github.io/cocina_display/
128
+ licenses:
129
+ - Apache-2.0
130
+ metadata:
131
+ homepage_uri: https://sul-dlss.github.io/cocina_display/
132
+ source_code_uri: https://github.com/sul-dlss/cocina_display
133
+ post_install_message:
134
+ rdoc_options: []
135
+ require_paths:
136
+ - lib
137
+ required_ruby_version: !ruby/object:Gem::Requirement
138
+ requirements:
139
+ - - ">="
140
+ - !ruby/object:Gem::Version
141
+ version: 3.1.0
142
+ required_rubygems_version: !ruby/object:Gem::Requirement
143
+ requirements:
144
+ - - ">="
145
+ - !ruby/object:Gem::Version
146
+ version: '0'
147
+ requirements: []
148
+ rubygems_version: 3.5.16
149
+ signing_key:
150
+ specification_version: 4
151
+ summary: Helpers for rendering Cocina metadata
152
+ test_files: []