├── .rspec ├── Gemfile ├── Rakefile ├── bin ├── setup └── console ├── .gitignore ├── spec ├── spec_helper.rb └── urn_spec.rb ├── .github └── workflows │ └── ci.yml ├── urn.gemspec ├── LICENSE.txt ├── lib └── urn.rb ├── CHANGELOG.md └── README.md /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | --require spec_helper 3 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in urn.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rspec/core/rake_task' 3 | 4 | RSpec::Core::RakeTask.new(:spec) 5 | 6 | task :default => :spec 7 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | 5 | bundle install 6 | 7 | # Do any other automated setup that you need to do here 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /spec/examples.txt 10 | /tmp/ 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "urn" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | RSpec.configure do |config| 2 | config.filter_run :focus 3 | config.run_all_when_everything_filtered = true 4 | config.example_status_persistence_file_path = "spec/examples.txt" 5 | config.disable_monkey_patching! 6 | config.warnings = true 7 | config.order = :random 8 | config.default_formatter = 'doc' if config.files_to_run.one? 9 | Kernel.srand config.seed 10 | 11 | config.expect_with :rspec do |expectations| 12 | expectations.include_chain_clauses_in_custom_matcher_descriptions = true 13 | end 14 | 15 | config.mock_with :rspec do |mocks| 16 | mocks.verify_partial_doubles = true 17 | end 18 | end 19 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI for URN Gem 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | 8 | jobs: 9 | test: 10 | runs-on: ubuntu-latest 11 | 12 | strategy: 13 | matrix: 14 | ruby-version: [2.3, 2.4, 2.5, 2.6, 2.7, 3.0, 3.1, 3.2, 3.3, jruby-9.3, jruby-9.4] 15 | 16 | steps: 17 | - name: Checkout code 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Ruby 21 | uses: ruby/setup-ruby@v1 22 | with: 23 | ruby-version: ${{ matrix.ruby-version }} 24 | bundler-cache: true 25 | 26 | - name: Install dependencies 27 | run: bundle install 28 | 29 | - name: Run tests 30 | run: bundle exec rake 31 | -------------------------------------------------------------------------------- /urn.gemspec: -------------------------------------------------------------------------------- 1 | Gem::Specification.new do |spec| 2 | spec.name = 'urn' 3 | spec.version = '2.0.2' 4 | spec.authors = ['Ana Castro', 'Jonathan Hernandez', 'Paul Mucur'] 5 | spec.email = 'support@altmetric.com' 6 | 7 | spec.summary = 'Utility methods to normalize and validate URNs' 8 | spec.homepage = 'https://github.com/altmetric/urn' 9 | spec.license = 'MIT' 10 | 11 | spec.files = Dir['*.{md,txt}', 'lib/**/*.rb'] 12 | spec.test_files = Dir['spec/**/*.rb'] 13 | spec.bindir = 'exe' 14 | 15 | spec.add_development_dependency('bundler', '>= 1.10') 16 | spec.add_development_dependency('rake', '~> 13.2') 17 | spec.add_development_dependency('rspec', '~> 3.13') 18 | end 19 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Altmetric LLP 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/urn.rb: -------------------------------------------------------------------------------- 1 | module Kernel 2 | def URN(urn) 3 | URN.new(urn) 4 | end 5 | end 6 | 7 | class URN 8 | InvalidURNError = Class.new(StandardError) 9 | 10 | PATTERN = %{(?i:urn:(?!urn:)[a-z0-9][\x00-\x7F]{1,31}:} + 11 | %{(?:[a-z0-9()+,-.:=@;$_!*'/]|%(?:2[1-9a-f]|[3-6][0-9a-f]|7[0-9a-e]))+)}.freeze 12 | REGEX = /\A#{PATTERN}\z/ 13 | 14 | attr_reader :urn, :nid, :nss 15 | private :urn 16 | 17 | def self.extract(str, &blk) 18 | str.scan(/\b#{PATTERN}/, &blk) 19 | end 20 | 21 | def initialize(urn) 22 | urn = urn.to_s 23 | fail InvalidURNError, "bad URN(is not URN?): #{urn}" if urn !~ REGEX 24 | 25 | @urn = urn 26 | _scheme, @nid, @nss = urn.split(':', 3) 27 | end 28 | 29 | def normalize 30 | normalized_nid = nid.downcase 31 | normalized_nss = nss.gsub(/%([0-9a-f]{2})/i) { |hex| hex.downcase } 32 | 33 | URN.new("urn:#{normalized_nid}:#{normalized_nss}") 34 | end 35 | 36 | def to_s 37 | urn 38 | end 39 | 40 | def ===(other) 41 | if other.respond_to?(:normalize) 42 | urn_string = other.normalize.to_s 43 | else 44 | begin 45 | urn_string = URN.new(other).normalize.to_s 46 | rescue URN::InvalidURNError 47 | return false 48 | end 49 | end 50 | 51 | normalize.to_s == urn_string 52 | end 53 | 54 | def ==(other) 55 | return false unless other.is_a?(URN) 56 | 57 | normalize.to_s == other.normalize.to_s 58 | end 59 | 60 | def eql?(other) 61 | return false unless other.is_a?(URN) 62 | 63 | to_s == other.to_s 64 | end 65 | end 66 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This 3 | project adheres to [Semantic Versioning](http://semver.org/). 4 | 5 | ## [2.0.2] - 2017-01-13 6 | ### Fixed 7 | - `.extract` now respects the word boundaries at the beginning of URNs. Example: `sideburn:mutton:chops` won't return `['urn:mutton:chops']` but `[]`. 8 | 9 | ## [2.0.1] - 2016-03-24 10 | ### Fixed 11 | - `URN()` shortcut works on classes without `Object` as ancestor. 12 | 13 | ## [2.0.0] - 2016-03-22 14 | ### Changed 15 | - Only return `URN` instances for valid `URN`s at creation. Raise `URN::InvalidURNError` otherwise. 16 | - `#normalize` returns a normalized `URN` object instead of its `String` representation. You can get the normalized `String` representation with `.normalize.to_s` 17 | - Do not allow hexadecimal numbers from 0 to 20 and from 7F to FF. See [RFC2141](https://www.ietf.org/rfc/rfc2141.txt) section "2.4 Excluded characters". 18 | 19 | ### Added 20 | - Shortcut method (`URN()`) at creation: 21 | ```ruby 22 | urn = URN('URN:Name:Spec') 23 | #=> # 24 | ``` 25 | - `REGEX` into the API documentation 26 | - `#nid` returns the namespace identifier part. 27 | ```ruby 28 | URN('URN:Name:Spec').nid 29 | #=> "Name" 30 | ``` 31 | - `#nss` returns the namespace specific string part. 32 | ```ruby 33 | URN('URN:Name:Spec').nss 34 | #=> "Spec" 35 | ``` 36 | - `#to_s` returns the `String` representation. 37 | ```ruby 38 | URN('URN:Name:Spec').to_s 39 | #=> "URN:Name:Spec" 40 | ``` 41 | - `#===(other)` returns true if the URN objects are equivalent. This method normalizes both URNs before doing the comparison, and allows comparison against Strings. 42 | - `#==(other)` returns true if the URN objects are equivalent. This method normalizes both URNs before doing the comparison. 43 | - `#eql?(other)` returns true if the URN objects are equal. This method does NOT normalize either URN before doing the comparison. 44 | - `.extract(str)` attempts to parse and merge a set of URNs. If no `block` is given, then returns the result as an `Array`. Else it calls `block` for each element in result and returns `nil`. 45 | - URN initialization accepts a `URN` as argument: 46 | ```ruby 47 | URN(URN('urn:foo:bar')) 48 | #=> # 49 | ``` 50 | 51 | ### Removed 52 | - `#valid?`. Validity check is now at object creation time, therefore all instances of `URN` are valid. 53 | 54 | 55 | ## [1.0.0] - 2016-03-09 56 | ### Changed 57 | - The library is now [RFC2141](https://www.ietf.org/rfc/rfc2141.txt) compliant. 58 | 59 | ## [0.1.3] - 2016-03-07 60 | ### Fixed 61 | - Explicitly require CGI standard library 62 | 63 | ## [0.1.2] - 2016-03-02 64 | ### Changed 65 | - Extract the `URN` pattern to a separate variable, so that it's easier to use it in different Regex formats if needed. 66 | 67 | ## [0.1.1] - 2016-03-02 68 | ### Changed 69 | - Stricter rules to validate a URN: it does not accept `urn` string as valid namespace identifier. 70 | 71 | ## [0.1.0] - 2016-03-01 72 | ### Added 73 | - First version with basic implementation. 74 | 75 | [2.0.2]: https://github.com/altmetric/urn/releases/tag/v2.0.2 76 | [2.0.1]: https://github.com/altmetric/urn/releases/tag/v2.0.1 77 | [2.0.0]: https://github.com/altmetric/urn/releases/tag/v2.0.0 78 | [1.0.0]: https://github.com/altmetric/urn/releases/tag/v1.0.0 79 | [0.1.3]: https://github.com/altmetric/urn/releases/tag/v0.1.3 80 | [0.1.2]: https://github.com/altmetric/urn/releases/tag/v0.1.2 81 | [0.1.1]: https://github.com/altmetric/urn/releases/tag/v0.1.1 82 | [0.1.0]: https://github.com/altmetric/urn/releases/tag/v0.1.0 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # URN [![Build Status](https://travis-ci.org/altmetric/urn.svg?branch=master)](https://travis-ci.org/altmetric/urn) 2 | 3 | Ruby library to validate and normalize URNs according to [RFC 2141](https://www.ietf.org/rfc/rfc2141.txt). 4 | 5 | [![Gem Version](https://badge.fury.io/rb/urn.svg)](https://badge.fury.io/rb/urn) 6 | 7 | **Supported Ruby versions:** >= 2.3 8 | 9 | ## Installation 10 | 11 | Add this line to your application's `Gemfile`: 12 | 13 | ```ruby 14 | gem 'urn', '~> 2.0' 15 | ``` 16 | 17 | And then execute: 18 | 19 | $ bundle 20 | 21 | Or install it yourself as: 22 | 23 | $ gem install urn -v '~> 2.0' 24 | 25 | ## Usage 26 | 27 | ```ruby 28 | urn = URN('URN:NameSpace:Identifier') 29 | #=> # 30 | 31 | urn = URN.new('URN:NameSpace:Identifier') 32 | #=> # 33 | 34 | urn.normalize 35 | #=> "urn:namespace:Identifier" 36 | 37 | urn = URN('123') 38 | #=> URN::InvalidURNError: bad URN(is not URN?): 123 39 | ``` 40 | 41 | ## API Documentation 42 | 43 | ### `URN::PATTERN` 44 | ```ruby 45 | text.match(/\?urn=(#{URN::PATTERN})/) 46 | ``` 47 | Return a `String` of an unanchored regular expression suitable for matching URNs. 48 | 49 | ### `URN::REGEX` 50 | ```ruby 51 | URN::REGEX 52 | #=> /\A(?i:urn:(?!urn:)[a-z0-9][a-z0-9-]{1,31}:(?:[a-z0-9()+,-.:=@;$_!*']|%[0-9a-f]{2})+)\z/ 53 | ``` 54 | Return a `Regexp` object with the anchored regular expression suitable to match a URN. 55 | 56 | ### `URN()` or `URN.new` 57 | ```ruby 58 | urn = URN('urn:nid:nss') 59 | #=> # 60 | 61 | urn = URN.new('urn:nid:nss') 62 | #=> # 63 | 64 | urn = URN('1234') 65 | #=> URN::InvalidURNError: bad URN(is not URN?): 1234 66 | ``` 67 | Return a new `URN` instance when the given string is valid according to [RFC 2141](https://www.ietf.org/rfc/rfc2141.txt). Otherwise, it raises an `URN::InvalidURNError` 68 | 69 | ### `URN#normalize` 70 | ```ruby 71 | URN('URN:FOO:BAR').normalize 72 | #=> # 73 | ``` 74 | Return the normalized `URN` object, normalizing the case 75 | of the `urn` token and namespace identifier. Call `#to_s` after `#normalize` if you want the normalized `String` representation. 76 | 77 | ### `URN#nid` 78 | ```ruby 79 | URN('urn:nid:nss').nid 80 | #=> "nid" 81 | ``` 82 | Return the namespace identifier part. 83 | 84 | ### `URN#nss` 85 | ```ruby 86 | URN('urn:nid:nss').nss 87 | #=> "nss" 88 | ``` 89 | Return the namespace specific string part. 90 | 91 | ### `URN#to_s` 92 | ```ruby 93 | URN('urn:Nid:Nss').to_s 94 | #=> "urn:Nid:Nss" 95 | ``` 96 | Return the `String` representation. 97 | 98 | ### `#===(other)` 99 | ```ruby 100 | URN('urn:name:spec') === 'URN:Name:spec' 101 | #=> true 102 | 103 | URN('urn:name:spec') === URN('URN:Name:spec') 104 | #=> true 105 | ``` 106 | Return true if the URN objects are equivalent. This method normalizes both URNs before doing the comparison, and allows comparison against Strings. 107 | 108 | ### `#==(other)` 109 | ```ruby 110 | URN('urn:name:spec') == 'URN:Name:spec' 111 | #=> false 112 | 113 | URN('urn:name:spec') == URN('URN:Name:spec') 114 | #=> true 115 | ``` 116 | Returns true if the URN objects are equivalent. This method normalizes both URNs before doing the comparison. 117 | 118 | ### `#eql?(other)` 119 | ```ruby 120 | URN('urn:name:spec').eql?('urn:name:spec') 121 | #=> false 122 | 123 | URN('urn:name:spec').eql?(URN('urn:NAME:spec')) 124 | #=> false 125 | 126 | URN('urn:name:spec').eql?(URN('urn:name:spec')) 127 | #=> true 128 | ``` 129 | Returns true if the URN objects are equal. This method does NOT normalize either URN before doing the comparison. 130 | 131 | ### `.extract(str)` 132 | ```ruby 133 | URN.extract('text urn:1234:abc more text URN:foo:bar%23.\\') 134 | #=> ['urn:1234:abc', 'URN:foo:bar%23.'] 135 | 136 | normalized_urns = [] 137 | #=> [] 138 | URN.extract('text urn:1234:abc more text URN:foo:bar%23.\\') { |urn| normalized_urns << URN(urn).normalize.to_s } 139 | #=> nil 140 | normalized_urns 141 | #=> ['urn:1234:abc', 'URN:foo:bar%23.'] 142 | ``` 143 | Extracts URNs from a string. If block given, iterates through all matched URNs. Returns nil if block given or array with matches. 144 | 145 | ## Contributing 146 | 147 | Bug reports and pull requests are welcome on GitHub at https://github.com/altmetric/urn. 148 | 149 | ## License 150 | 151 | Copyright © 2016-2024 Altmetric LLP 152 | 153 | Distributed under the [MIT License](http://opensource.org/licenses/MIT). 154 | -------------------------------------------------------------------------------- /spec/urn_spec.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | require 'urn' 3 | require 'uri' 4 | 5 | RSpec.describe URN do 6 | describe 'URN' do 7 | it 'returns a URN if it is valid' do 8 | expect(URN('urn:namespace:specificstring')).to be_kind_of(described_class) 9 | end 10 | 11 | it 'raise InvalidURNError if it is not valid' do 12 | expect { URN('urn:urn:1234') }.to raise_error(described_class::InvalidURNError, 'bad URN(is not URN?): urn:urn:1234') 13 | end 14 | 15 | it 'returns the same URN if the argument is a URN' do 16 | urn = URN.new('urn:foo:bar') 17 | 18 | expect(URN(urn).to_s).to eq('urn:foo:bar') 19 | end 20 | end 21 | 22 | describe '#initialize' do 23 | it 'returns the same URN if the argument is a URN' do 24 | urn = URN.new('urn:foo:bar') 25 | 26 | expect(URN.new(urn).to_s).to eq('urn:foo:bar') 27 | end 28 | 29 | it 'returns a URN if it is valid' do 30 | expect(described_class.new('urn:namespace:specificstring')).to be_kind_of(described_class) 31 | end 32 | 33 | it 'returns a URN if NSS includes /' do 34 | expect(described_class.new('urn:namespace:specificstring/withbar')).to be_kind_of(described_class) 35 | end 36 | 37 | it 'returns a URN if NID includes ascii chars' do 38 | expect(described_class.new('urn:namespace.,:specificstring/withbar')).to be_kind_of(described_class) 39 | end 40 | 41 | it 'raise InvalidURNError if it is not valid' do 42 | expect { described_class.new('urn:urn:1234') }.to raise_error(described_class::InvalidURNError, 'bad URN(is not URN?): urn:urn:1234') 43 | end 44 | 45 | it 'returns a URN if namespace includes urn' do 46 | expect(described_class.new('urn:urnnamespace:specificstring')).to be_kind_of(described_class) 47 | end 48 | 49 | it 'raises an error if it does not start with urn' do 50 | expect { described_class.new('not-urn:namespace:specificstring') }.to raise_error(described_class::InvalidURNError) 51 | end 52 | 53 | it 'raises an error if namespace is urn' do 54 | expect { described_class.new('urn:urn:specificstring') }.to raise_error(described_class::InvalidURNError) 55 | end 56 | 57 | it 'raises an error if namespace is URN' do 58 | expect { described_class.new('urn:URN:specificstring') }.to raise_error(described_class::InvalidURNError) 59 | end 60 | 61 | it 'returns true if the namespace identifier is 32 characters long' do 62 | nid = 'a' * 32 63 | 64 | expect(described_class.new("urn:#{nid}:bar")).to be_kind_of(described_class) 65 | end 66 | 67 | it 'raises an error if the namespace identifier begins with a hyphen' do 68 | expect { described_class.new('urn:-foo:bar') }.to raise_error(described_class::InvalidURNError) 69 | end 70 | 71 | it 'raises an error if the namespace specific string has invalid escaping' do 72 | expect { described_class.new('urn:foo:bar%2') }.to raise_error(described_class::InvalidURNError) 73 | end 74 | 75 | it 'raises an error if the namespace specific string has reserved characters' do 76 | expect { described_class.new('urn:foo:café') }.to raise_error(described_class::InvalidURNError) 77 | end 78 | 79 | it 'raises an error if the namespace specific string has a not allowed hexadecimal value' do 80 | expect { described_class.new('urn:foo:abc%10') }.to raise_error(described_class::InvalidURNError) 81 | end 82 | end 83 | 84 | describe '#normalize' do 85 | it 'lowercases the leading "urn:" token' do 86 | expect(described_class.new('URN:foo:123').normalize.to_s).to eq('urn:foo:123') 87 | end 88 | 89 | it 'lowercases the namespace identifier' do 90 | expect(described_class.new('urn:FOO:123').normalize.to_s).to eq('urn:foo:123') 91 | end 92 | 93 | it 'lowercases %-escaping in the namespace specific string' do 94 | expect(described_class.new('urn:foo:123%2C456').normalize.to_s).to eq('urn:foo:123%2c456') 95 | end 96 | 97 | it 'does not lowercase other characters in the namespace specific string' do 98 | expect(described_class.new('urn:foo:BA%2CR').normalize.to_s).to eq('urn:foo:BA%2cR') 99 | end 100 | end 101 | 102 | describe '#nid' do 103 | it 'returns the namespace identifier' do 104 | expect(described_class.new('urn:namespace:specificstring').nid).to eq('namespace') 105 | end 106 | end 107 | 108 | describe '#nss' do 109 | it 'returns the namespace specific string' do 110 | expect(described_class.new('urn:namespace:specificstring').nss).to eq('specificstring') 111 | end 112 | end 113 | 114 | describe '#to_s' do 115 | it 'returns the string representation of the URN' do 116 | expect(described_class.new('urn:Name:Spec').to_s).to eq('urn:Name:Spec') 117 | end 118 | end 119 | 120 | describe '#===' do 121 | it 'returns true if the string is an equivalent valid URN' do 122 | expect(described_class.new('urn:name:spec') === 'URN:Name:spec').to be(true) 123 | end 124 | 125 | it 'returns true if the normalized object of the other class is equal' do 126 | expect(described_class.new('urn:name:spec') === URI('urn:name:spec')).to be(true) 127 | end 128 | 129 | it 'returns true if the URN object is an equivalent valid URN' do 130 | expect(described_class.new('urn:name:spec') === described_class.new('URN:Name:spec')).to be(true) 131 | end 132 | 133 | it 'returns false if the URN is not equivalent' do 134 | expect(described_class.new('urn:name:spec') === described_class.new('URN:Name:SPEC')).to be(false) 135 | end 136 | 137 | it 'return false if the URN is not valid' do 138 | expect(described_class.new('urn:name:spec') === 'urn:urn:urn').to be(false) 139 | end 140 | end 141 | 142 | describe '#==' do 143 | it 'returns true if the URN object is an equivalent valid URN' do 144 | expect(described_class.new('urn:name:spec')).to eq(described_class.new('URN:Name:spec')) 145 | end 146 | 147 | it 'returns false if the argument is not a URN object' do 148 | expect(described_class.new('urn:name:spec')).not_to eq('urn:name:spec') 149 | end 150 | end 151 | 152 | describe '#eql?' do 153 | it 'returns true if both URNs are equal' do 154 | expect(described_class.new('urn:Name:Spec')).to eql(described_class.new('urn:Name:Spec')) 155 | end 156 | 157 | it 'returns false if the URNs are not equal' do 158 | expect(described_class.new('urn:name:spec')).not_to eql(described_class.new('urn:Name:spec')) 159 | end 160 | end 161 | 162 | describe '.extract' do 163 | it 'extracts the URNs from a string' do 164 | str = 'En un pueblo italiano urn:1234:abc al pie de la montaña URN:foo:bar%23.\\' 165 | 166 | expect(URN.extract(str)).to contain_exactly('urn:1234:abc', 'URN:foo:bar%23.') 167 | end 168 | 169 | it 'extracts the URNs from a string using a block' do 170 | str = 'Llum, foc, destrucció. urn:foo:%10 El món pot ser només una runa, URN:FOO:BA%2cR això no ho consentirem.' 171 | 172 | normalized_urns = [] 173 | URN.extract(str) { |urn| normalized_urns << URN(urn).normalize.to_s } 174 | 175 | expect(normalized_urns).to contain_exactly('urn:foo:BA%2cR') 176 | end 177 | 178 | it 'only extracts URNs with word boundaries at the beginning' do 179 | expect(URN.extract('sideburn:mutton:chops')).to be_empty 180 | end 181 | end 182 | end 183 | --------------------------------------------------------------------------------