Skip to content

Commit

Permalink
Merge pull request #640 from sul-dlss/iss637-add-bcp47-language-tag-t…
Browse files Browse the repository at this point in the history
…o-files

add BCP 47 language tag attribute for files
  • Loading branch information
jmartin-sul authored Nov 1, 2023
2 parents d50e32d + 87c1830 commit 56713f0
Show file tree
Hide file tree
Showing 10 changed files with 183 additions and 11 deletions.
3 changes: 3 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,9 @@ Naming/PredicateName:

# ----- RSpec ------

RSpec/NestedGroups:
Max: 5

RSpec/BeEq: # new in 2.9.0
Enabled: true

Expand Down
13 changes: 3 additions & 10 deletions .rubocop_todo.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude`
# on 2023-06-20 05:46:00 UTC using RuboCop version 1.52.1.
# on 2023-11-01 19:51:40 UTC using RuboCop version 1.57.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
Expand Down Expand Up @@ -57,18 +57,11 @@ Metrics/ParameterLists:
RSpec/DescribeClass:
Enabled: false

# Offense count: 87
# Offense count: 224
# Configuration parameters: CountAsOne.
RSpec/ExampleLength:
Max: 103

# Offense count: 10
# Configuration parameters: Max, AllowedGroups.
RSpec/NestedGroups:
Exclude:
- 'spec/cocina/models/mapping/normalizers/mods/origin_info_normalizer_spec.rb'
- 'spec/cocina/models/validators/date_time_validator_spec.rb'

# Offense count: 19
RSpec/PendingWithoutReason:
Exclude:
Expand Down Expand Up @@ -99,7 +92,7 @@ Style/MultilineBlockChain:
- 'lib/cocina/models/mapping/to_mods/form.rb'
- 'lib/cocina/models/mapping/to_mods/subject.rb'

# Offense count: 239
# Offense count: 249
# This cop supports safe autocorrection (--autocorrect).
# Configuration parameters: AllowHeredoc, AllowURI, URISchemes, IgnoreCopDirectives, AllowedPatterns.
# URISchemes: http, https
Expand Down
1 change: 1 addition & 0 deletions Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ PATH
dry-types (~> 1.1)
edtf
equivalent-xml
i18n
jsonpath
nokogiri
openapi3_parser
Expand Down
1 change: 1 addition & 0 deletions cocina-models.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Gem::Specification.new do |spec|
spec.add_dependency 'dry-types', '~> 1.1'
spec.add_dependency 'edtf' # used for date/time validation
spec.add_dependency 'equivalent-xml' # for diffing MODS
spec.add_dependency 'i18n' # for validating BCP 47 language tags, according to RFC 4646
spec.add_dependency 'jsonpath' # used for date/time validation
spec.add_dependency 'nokogiri'
spec.add_dependency 'openapi3_parser' # Parsing openapi doc
Expand Down
2 changes: 2 additions & 0 deletions lib/cocina/models/file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ class File < Struct
attribute :version, Types::Strict::Integer
# MIME Type of the File.
attribute? :hasMimeType, Types::Strict::String
# BCP 47 language tag: https://www.rfc-editor.org/rfc/rfc4646.txt -- other applications (like media players) expect language codes of this format, see e.g. https://videojs.com/guides/text-tracks/#srclang
attribute? :languageTag, Types::Strict::String.optional
# Use for the File.
attribute? :use, Types::Strict::String
attribute :hasMessageDigests, Types::Strict::Array.of(MessageDigest).default([].freeze)
Expand Down
1 change: 1 addition & 0 deletions lib/cocina/models/request_file.rb
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ class RequestFile < Struct
attribute? :size, Types::Strict::Integer
attribute :version, Types::Strict::Integer
attribute? :hasMimeType, Types::Strict::String
attribute? :languageTag, Types::Strict::String.optional
attribute? :externalIdentifier, Types::Strict::String
attribute? :use, Types::Strict::String
attribute :hasMessageDigests, Types::Strict::Array.of(MessageDigest).default([].freeze)
Expand Down
48 changes: 48 additions & 0 deletions lib/cocina/models/validators/language_tag_validator.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# frozen_string_literal: true

module Cocina
module Models
module Validators
# Validates that a languageTag is valid according to RFC 4646, if one is present
class LanguageTagValidator
def self.validate(clazz, attributes)
new(clazz, attributes).validate
end

def initialize(clazz, attributes)
@clazz = clazz
@attributes = attributes
end

def validate
return unless meets_preconditions?

return if valid_language_tag?

raise ValidationError, 'The provided language tag is not valid according to RFC 4646: ' \
"#{attributes[:languageTag]}"
end

private

attr_reader :clazz, :attributes

def meets_preconditions?
file? && attributes[:languageTag].present?
end

def file?
(clazz::TYPES & File::TYPES).any?
rescue NameError
false
end

def valid_language_tag?
parsed_tag = I18n::Locale::Tag::Rfc4646.tag(attributes[:languageTag])

parsed_tag.present? && parsed_tag.is_a?(I18n::Locale::Tag::Rfc4646)
end
end
end
end
end
3 changes: 2 additions & 1 deletion lib/cocina/models/validators/validator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ class Validator
# See also spec/cocina/models/validatable_spec.rb:59
# DescriptionTypesValidator,
DescriptionValuesValidator,
DateTimeValidator
DateTimeValidator,
LanguageTagValidator
].freeze

def self.validate(clazz, attributes)
Expand Down
5 changes: 5 additions & 0 deletions openapi.yml
Original file line number Diff line number Diff line change
Expand Up @@ -1044,6 +1044,9 @@ components:
hasMimeType:
description: MIME Type of the File.
type: string
languageTag:
description: "BCP 47 language tag: https://www.rfc-editor.org/rfc/rfc4646.txt -- other applications (like media players) expect language codes of this format, see e.g. https://videojs.com/guides/text-tracks/#srclang"
type: string
use:
description: Use for the File.
type: string
Expand Down Expand Up @@ -1775,6 +1778,8 @@ components:
type: integer
hasMimeType:
type: string
languageTag:
type: string
externalIdentifier:
type: string
use:
Expand Down
117 changes: 117 additions & 0 deletions spec/cocina/models/validators/language_tag_validator_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
# frozen_string_literal: true

require 'spec_helper'

RSpec.describe Cocina::Models::Validators::LanguageTagValidator do
let(:validate) { described_class.validate(clazz, props) }

let(:props) { file_props }

let(:file_props) do
{
externalIdentifier: 'bc123df4567_1',
label: 'Page 1',
type: Cocina::Models::ObjectType.file,
version: 1,
access: { view: 'dark', download: 'none' },
administrative: {
publish: false,
shelve: false,
sdrPreserve: true
},
hasMessageDigests: [],
hasMimeType: 'text/plain',
filename: 'page1.txt'
}
end

context 'with no value for languageTag specified' do
context 'with a File' do
let(:clazz) { Cocina::Models::File }

it 'does not raise' do
expect { validate }.not_to raise_error
end
end

context 'with a RequestFile' do
let(:clazz) { Cocina::Models::RequestFile }

it 'does not raise' do
expect { validate }.not_to raise_error
end
end
end

context 'with RFC 4646 conformant value for languageTag specified' do
let(:props) do
file_props.dup.tap do |props|
props[:languageTag] = language_tag
end
end

context 'with a recognized language, script, and region' do
let(:language_tag) { 'ru-Cyrl-RU' }

context 'with a File' do
let(:clazz) { Cocina::Models::File }

it 'does not raise' do
expect { validate }.not_to raise_error
end
end

context 'with a RequestFile' do
let(:clazz) { Cocina::Models::RequestFile }

it 'does not raise' do
expect { validate }.not_to raise_error
end
end
end

context 'with an unrecognized language, script, and region' do
let(:language_tag) { 'foo-Barr-BZ' } # still conforms to the expected format of BCP 47/RFC 4646, should parse

context 'with a File' do
let(:clazz) { Cocina::Models::File }

it 'does not raise' do
expect { validate }.not_to raise_error
end
end

context 'with a RequestFile' do
let(:clazz) { Cocina::Models::RequestFile }

it 'does not raise' do
expect { validate }.not_to raise_error
end
end
end
end

context 'with value for languageTag specified that does not conform to RFC 4646' do
let(:props) do
file_props.dup.tap do |props|
props[:languageTag] = 'fooooooooooooo'
end
end

context 'with a File' do
let(:clazz) { Cocina::Models::File }

it 'raises a validation error' do
expect { validate }.to raise_error(Cocina::Models::ValidationError, 'The provided language tag is not valid according to RFC 4646: fooooooooooooo')
end
end

context 'with a RequestFile' do
let(:clazz) { Cocina::Models::RequestFile }

it 'raises a validation error' do
expect { validate }.to raise_error(Cocina::Models::ValidationError, 'The provided language tag is not valid according to RFC 4646: fooooooooooooo')
end
end
end
end

0 comments on commit 56713f0

Please sign in to comment.