sip2 0.2.1 → 0.2.2
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 +4 -4
- data/README.md +104 -0
- data/lib/sip2.rb +5 -1
- data/lib/sip2/client.rb +1 -3
- data/lib/sip2/connection.rb +11 -29
- data/lib/sip2/messages/base.rb +64 -0
- data/lib/sip2/messages/login.rb +19 -9
- data/lib/sip2/messages/patron_information.rb +19 -10
- data/lib/sip2/messages/status.rb +55 -0
- data/lib/sip2/responses/base.rb +114 -0
- data/lib/sip2/responses/patron_information.rb +144 -0
- data/lib/sip2/responses/status.rb +133 -0
- data/lib/sip2/version.rb +1 -1
- metadata +40 -7
- data/lib/sip2/patron_information.rb +0 -202
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1e096d30ed5962669bb45c1747f83339682f4a3fa8067e35301ea3a975de13c
|
4
|
+
data.tar.gz: 7be6116ba6402c005ea78a0dc131dca72762cb593d083529b63dac82ed88de00
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 03ad63e45484c5cf767ba8f261b2b8b90155721b40d789d1d8b2418ad26396f768272a3584f987d628779bce09f83f39ac94d0ab04f109dc8c981340fabfd1c3
|
7
|
+
data.tar.gz: 7c1e703837a65efd8c4fa02bad6f589fbc4e8f48d9d81627f90da31bf3f66fe7216704d448756c62e3159bfca7d2cf7d99566aba34797eaa562de6c7c8c63cd1
|
data/README.md
ADDED
@@ -0,0 +1,104 @@
|
|
1
|
+
[](https://travis-ci.org/Studiosity/sip2-ruby)
|
2
|
+
[](#)
|
3
|
+
|
4
|
+
# 3M™ Standard Interchange Protocol v2 (SIP2) client implementation in Ruby
|
5
|
+
|
6
|
+
This is a gem wrapping the SIP v2 protocol.
|
7
|
+
|
8
|
+
http://multimedia.3m.com/mws/media/355361O/sip2-protocol.pdf
|
9
|
+
|
10
|
+
|
11
|
+
## Installation
|
12
|
+
|
13
|
+
Add this line to your application's Gemfile:
|
14
|
+
|
15
|
+
```ruby
|
16
|
+
gem 'sip2'
|
17
|
+
```
|
18
|
+
|
19
|
+
And then execute:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
$ bundle
|
23
|
+
```
|
24
|
+
|
25
|
+
|
26
|
+
## Protocol support
|
27
|
+
|
28
|
+
So far only login (code 93) and patron_information (code 63) are supported
|
29
|
+
|
30
|
+
|
31
|
+
## Usage
|
32
|
+
|
33
|
+
```ruby
|
34
|
+
client = Sip2::Client.new(host: 'my.sip2.host.net', port: 6001)
|
35
|
+
patron =
|
36
|
+
client.connect do |connection|
|
37
|
+
if connection.login 'sip_username', 'sip_password'
|
38
|
+
connection.patron_information 'patron_username', 'patron_password'
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
puts 'Valid patron' if patron && patron.authenticated?
|
43
|
+
```
|
44
|
+
|
45
|
+
### Using TLS
|
46
|
+
|
47
|
+
The Sip2::Client will use TLS if a
|
48
|
+
[SSLContext](https://ruby-doc.org/stdlib-2.4.10/libdoc/openssl/rdoc/OpenSSL/SSL/SSLContext.html)
|
49
|
+
is passed in the `ssl_context` parameter. There are quite a few ways this can be configured, but that will
|
50
|
+
depend on how the server being connected to is configured. A basic example is:
|
51
|
+
|
52
|
+
```ruby
|
53
|
+
# Setup a cert store using the system certificates/trust chain
|
54
|
+
cert_store = OpenSSL::X509::Store.new
|
55
|
+
cert_store.set_default_paths
|
56
|
+
|
57
|
+
# Setup the SSL context
|
58
|
+
ssl_context = OpenSSL::SSL::SSLContext.new
|
59
|
+
ssl_context.verify_mode = OpenSSL::SSL::VERIFY_PEER # This is important. We want to verify the certificate is legitimate
|
60
|
+
ssl_context.min_version = OpenSSL::SSL::TLS1_2_VERSION # Generally good practice to enforce the most recent TLS version
|
61
|
+
ssl_context.cert_store = cert_store # Use the certificate store we configured above
|
62
|
+
|
63
|
+
# Raise an exception if the certificate doesn't check out
|
64
|
+
ssl_context.verify_callback = proc do |preverify_ok, context|
|
65
|
+
raise OpenSSL::SSL::SSLError, <<~ERROR.strip if preverify_ok != true || context.error != 0
|
66
|
+
SSL Verification failed -- Preverify: #{preverify_ok}, Error: #{context.error_string} (#{context.error})
|
67
|
+
ERROR
|
68
|
+
|
69
|
+
true
|
70
|
+
end
|
71
|
+
|
72
|
+
client = Sip2::Client.new(host: 'my.sip2.host.net', port: 6001, ssl_context: ssl_context)
|
73
|
+
```
|
74
|
+
|
75
|
+
If you needed to explicitly specify the certificate to be used there are a few options available
|
76
|
+
to use instead of cert_store (see the documentation for full details and other options):
|
77
|
+
* ca_file - path to a file containing a CA certificate
|
78
|
+
* ca_path - path to a directory containing CA certificates
|
79
|
+
* client_ca - a certificate or array of certificates
|
80
|
+
* client_cert_cb - callback where an array containing an X509 certificate and key are returned
|
81
|
+
|
82
|
+
Be sure to validate that your setup behaves the way you expect it to.
|
83
|
+
Pass in an invalid certificate and see it fails. Pass a mismatching hostname and see it fails. etc.
|
84
|
+
|
85
|
+
## Contributing
|
86
|
+
|
87
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/Studiosity/sip2-ruby.
|
88
|
+
|
89
|
+
Note that spec tests are appreciated to minimise regressions. Before submitting a PR, please ensure that:
|
90
|
+
|
91
|
+
```bash
|
92
|
+
$ rspec
|
93
|
+
```
|
94
|
+
and
|
95
|
+
|
96
|
+
```bash
|
97
|
+
$ rubocop
|
98
|
+
```
|
99
|
+
both succeed
|
100
|
+
|
101
|
+
|
102
|
+
## License
|
103
|
+
|
104
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
data/lib/sip2.rb
CHANGED
@@ -4,10 +4,14 @@ require 'sip2/version'
|
|
4
4
|
|
5
5
|
require 'openssl'
|
6
6
|
|
7
|
-
require 'sip2/
|
7
|
+
require 'sip2/responses/base'
|
8
|
+
require 'sip2/responses/patron_information'
|
9
|
+
require 'sip2/responses/status'
|
8
10
|
|
11
|
+
require 'sip2/messages/base'
|
9
12
|
require 'sip2/messages/login'
|
10
13
|
require 'sip2/messages/patron_information'
|
14
|
+
require 'sip2/messages/status'
|
11
15
|
|
12
16
|
module Sip2
|
13
17
|
class TimeoutError < StandardError; end
|
data/lib/sip2/client.rb
CHANGED
@@ -13,8 +13,7 @@ module Sip2
|
|
13
13
|
@ssl_context = ssl_context
|
14
14
|
end
|
15
15
|
|
16
|
-
# rubocop:disable Metrics/MethodLength
|
17
|
-
def connect
|
16
|
+
def connect # rubocop:disable Metrics/MethodLength
|
18
17
|
socket = NonBlockingSocket.connect host: @host, port: @port, timeout: @timeout
|
19
18
|
|
20
19
|
# If we've been provided with an SSL context then use it to wrap out existing connection
|
@@ -32,6 +31,5 @@ module Sip2
|
|
32
31
|
ensure
|
33
32
|
socket&.close
|
34
33
|
end
|
35
|
-
# rubocop:enable Metrics/MethodLength
|
36
34
|
end
|
37
35
|
end
|
data/lib/sip2/connection.rb
CHANGED
@@ -7,19 +7,6 @@ module Sip2
|
|
7
7
|
class Connection
|
8
8
|
LINE_SEPARATOR = "\r"
|
9
9
|
|
10
|
-
@connection_modules = []
|
11
|
-
|
12
|
-
class << self
|
13
|
-
attr_reader :connection_modules
|
14
|
-
|
15
|
-
def add_connection_module(module_name)
|
16
|
-
@connection_modules << module_name
|
17
|
-
end
|
18
|
-
end
|
19
|
-
|
20
|
-
include Messages::Login
|
21
|
-
include Messages::PatronInformation
|
22
|
-
|
23
10
|
def initialize(socket:, ignore_error_detection: false)
|
24
11
|
@socket = socket
|
25
12
|
@ignore_error_detection = ignore_error_detection
|
@@ -27,33 +14,29 @@ module Sip2
|
|
27
14
|
end
|
28
15
|
|
29
16
|
def send_message(message)
|
17
|
+
message = with_checksum with_error_detection message
|
30
18
|
write_with_timeout message
|
31
|
-
read_with_timeout
|
19
|
+
response = read_with_timeout
|
20
|
+
response if sequence_and_checksum_valid? response
|
21
|
+
ensure
|
22
|
+
@sequence += 1
|
32
23
|
end
|
33
24
|
|
34
25
|
def method_missing(method_name, *args)
|
35
|
-
|
36
|
-
|
37
|
-
else
|
26
|
+
message_class = Messages::Base.message_class_for_method(method_name)
|
27
|
+
if message_class.nil?
|
38
28
|
super
|
29
|
+
else
|
30
|
+
message_class.new(self).action_message(*args)
|
39
31
|
end
|
40
32
|
end
|
41
33
|
|
42
34
|
def respond_to_missing?(method_name, _include_private = false)
|
43
|
-
|
35
|
+
!Messages::Base.message_class_for_method(method_name).nil? || super
|
44
36
|
end
|
45
37
|
|
46
38
|
private
|
47
39
|
|
48
|
-
def send_and_handle_message(message_type, *args)
|
49
|
-
message = send "build_#{message_type}_message", *args
|
50
|
-
message = with_checksum with_error_detection message
|
51
|
-
response = send_message message
|
52
|
-
return if response.nil?
|
53
|
-
|
54
|
-
send "handle_#{message_type}_response", response
|
55
|
-
end
|
56
|
-
|
57
40
|
def write_with_timeout(message, separator: LINE_SEPARATOR)
|
58
41
|
::Timeout.timeout connection_timeout, WriteTimeout do
|
59
42
|
@socket.write message + separator
|
@@ -92,14 +75,13 @@ module Sip2
|
|
92
75
|
|
93
76
|
def sequence_and_checksum_valid?(response)
|
94
77
|
return true if @ignore_error_detection
|
78
|
+
return false unless response.is_a? String
|
95
79
|
|
96
80
|
sequence_regex = /\A(?<message>.*?AY(?<sequence>[0-9]+)AZ)(?<checksum>[A-F0-9]{4})\z/
|
97
81
|
match = response.strip.match sequence_regex
|
98
82
|
match &&
|
99
83
|
match[:sequence] == @sequence.to_s &&
|
100
84
|
match[:checksum] == checksum_for(match[:message])
|
101
|
-
ensure
|
102
|
-
@sequence += 1
|
103
85
|
end
|
104
86
|
end
|
105
87
|
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sip2
|
4
|
+
module Messages
|
5
|
+
#
|
6
|
+
# Sip2 Base message handler
|
7
|
+
#
|
8
|
+
class Base
|
9
|
+
@message_class_lookup = {}
|
10
|
+
|
11
|
+
# Class helper to fetch the descendant "message" class based on a method name
|
12
|
+
#
|
13
|
+
# @param [String] method_name the underscore case name of the class to fetch
|
14
|
+
# @return [Class] the message class fetched based on the method_name,
|
15
|
+
# `nil` if no descendant was found
|
16
|
+
# @example
|
17
|
+
# message_class_for_method('patron_information')
|
18
|
+
# => Sip2::Messages::PatronInformation
|
19
|
+
def self.message_class_for_method(method_name) # rubocop:disable Metrics/MethodLength
|
20
|
+
return @message_class_lookup[method_name] if @message_class_lookup.key? :method_name
|
21
|
+
|
22
|
+
@message_class_lookup[method_name] =
|
23
|
+
begin
|
24
|
+
# classify the method name so we can fetch the message class of the same name
|
25
|
+
class_name = method_name.to_s.capitalize.gsub(%r{(?:_|(/))([a-z\d]*)}i) do
|
26
|
+
"#{Regexp.last_match(1)}#{Regexp.last_match(2).capitalize}"
|
27
|
+
end
|
28
|
+
message_class = Messages.const_get(class_name)
|
29
|
+
message_class if message_class && message_class < self
|
30
|
+
rescue NameError
|
31
|
+
nil
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def initialize(connection)
|
36
|
+
@connection = connection
|
37
|
+
end
|
38
|
+
|
39
|
+
# Action the message, passing the dynamic arguments to the specific message implementation
|
40
|
+
#
|
41
|
+
# @param [*various] args Arguments to pass to the specific message implementation
|
42
|
+
# @return returns `nil` if there was no valid message returned.
|
43
|
+
# Otherwise value will depend on the specific message. See the `handle_response`
|
44
|
+
# method in those classes for more information
|
45
|
+
def action_message(*args)
|
46
|
+
message = build_message(*args)
|
47
|
+
response = @connection.send_message message
|
48
|
+
return if response.nil?
|
49
|
+
|
50
|
+
handle_response(response)
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def build_message(*)
|
56
|
+
raise NotImplementedError, "#{self.class} must implement `build_message` method"
|
57
|
+
end
|
58
|
+
|
59
|
+
def handle_response(_response)
|
60
|
+
raise NotImplementedError, "#{self.class} must implement `handle_response` method"
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
data/lib/sip2/messages/login.rb
CHANGED
@@ -3,16 +3,26 @@
|
|
3
3
|
module Sip2
|
4
4
|
module Messages
|
5
5
|
#
|
6
|
-
# Sip2 Login message
|
6
|
+
# Sip2 Login message
|
7
7
|
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
# https://developers.exlibrisgroup.com/wp-content/uploads/2020/01/3M-Standard-Interchange-Protocol-Version-2.00.pdf
|
9
|
+
#
|
10
|
+
# Request message 93
|
11
|
+
# * UID algorithm - 1 char, fixed-length required field; the algorithm used
|
12
|
+
# to encrypt the user id
|
13
|
+
# * PWD algorithm - 1 char, fixed-length required field; the algorithm used
|
14
|
+
# to encrypt the password
|
15
|
+
# * login user id - CN - variable-length required field
|
16
|
+
# * login password - CO - variable-length required field
|
17
|
+
# * location code - CP - variable-length required field
|
18
|
+
#
|
19
|
+
# Response message 94
|
20
|
+
# * ok - 1 char, fixed-length required field: 0 or 1
|
21
|
+
#
|
22
|
+
class Login < Base
|
13
23
|
private
|
14
24
|
|
15
|
-
def
|
25
|
+
def build_message(username:, password:, location_code: nil)
|
16
26
|
code = '93' # Login
|
17
27
|
uid_algorithm = pw_algorithm = '0' # Plain text
|
18
28
|
username_field = "CN#{username}"
|
@@ -25,8 +35,8 @@ module Sip2
|
|
25
35
|
].join
|
26
36
|
end
|
27
37
|
|
28
|
-
def
|
29
|
-
|
38
|
+
def handle_response(response)
|
39
|
+
response[/\A94([01])AY/, 1] == '1'
|
30
40
|
end
|
31
41
|
end
|
32
42
|
end
|
@@ -3,16 +3,25 @@
|
|
3
3
|
module Sip2
|
4
4
|
module Messages
|
5
5
|
#
|
6
|
-
# Sip2 Patron information message
|
6
|
+
# Sip2 Patron information message
|
7
7
|
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
8
|
+
# https://developers.exlibrisgroup.com/wp-content/uploads/2020/01/3M-Standard-Interchange-Protocol-Version-2.00.pdf
|
9
|
+
#
|
10
|
+
# Request message 63
|
11
|
+
# * language - 3 char, fixed-length required field
|
12
|
+
# * transaction date - 18 char, fixed-length required field: YYYYMMDDZZZZHHMMSS
|
13
|
+
# * summary - 10 char, fixed-length required field
|
14
|
+
# * institution id - AO - variable-length required field
|
15
|
+
# * patron identifier - AA - variable-length required field
|
16
|
+
# * terminal password - AC - variable-length optional field
|
17
|
+
# * patron password - AD - variable-length optional field
|
18
|
+
# * start item - BP - variable-length optional field
|
19
|
+
# * end item - BQ - variable-length optional field
|
20
|
+
#
|
21
|
+
class PatronInformation < Base
|
13
22
|
private
|
14
23
|
|
15
|
-
def
|
24
|
+
def build_message(uid:, password:, terminal_password: nil)
|
16
25
|
code = '63' # Patron information
|
17
26
|
language = '000' # Unknown
|
18
27
|
timestamp = Time.now.strftime('%Y%m%d %H%M%S')
|
@@ -23,10 +32,10 @@ module Sip2
|
|
23
32
|
].join
|
24
33
|
end
|
25
34
|
|
26
|
-
def
|
27
|
-
return unless
|
35
|
+
def handle_response(response)
|
36
|
+
return unless /\A#{Sip2::Responses::PatronInformation::RESPONSE_ID}/o.match?(response)
|
28
37
|
|
29
|
-
Sip2::PatronInformation.new response
|
38
|
+
Sip2::Responses::PatronInformation.new response
|
30
39
|
end
|
31
40
|
end
|
32
41
|
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sip2
|
4
|
+
module Messages
|
5
|
+
#
|
6
|
+
# Sip2 Patron information message
|
7
|
+
#
|
8
|
+
# https://developers.exlibrisgroup.com/wp-content/uploads/2020/01/3M-Standard-Interchange-Protocol-Version-2.00.pdf
|
9
|
+
#
|
10
|
+
# Request message 99
|
11
|
+
# * status code - 1 char, fixed-length required field: 0, 1 or 2
|
12
|
+
# * max print width - 3 char, fixed-length required field
|
13
|
+
# * protocol version - 4 char, fixed-length required field: x.xx
|
14
|
+
#
|
15
|
+
class Status < Base
|
16
|
+
STATUS_CODE_LOOKUP = {
|
17
|
+
ok: 0,
|
18
|
+
out_of_paper: 1,
|
19
|
+
about_to_shut_down: 2
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def build_message(status_code: :ok, max_print_width: 999, protocol_version: 2)
|
25
|
+
[
|
26
|
+
'99', # SC Status
|
27
|
+
normalize_status_code(status_code),
|
28
|
+
normalize_print_width(max_print_width),
|
29
|
+
normalize_protocol_version(protocol_version)
|
30
|
+
].join
|
31
|
+
end
|
32
|
+
|
33
|
+
def normalize_status_code(code)
|
34
|
+
format(
|
35
|
+
'%<code>d',
|
36
|
+
code: (code.is_a?(Symbol) ? STATUS_CODE_LOOKUP[code] : code).to_i.abs
|
37
|
+
)[0]
|
38
|
+
end
|
39
|
+
|
40
|
+
def normalize_print_width(width)
|
41
|
+
format('%03<width>d', width: width % 1000)
|
42
|
+
end
|
43
|
+
|
44
|
+
def normalize_protocol_version(version)
|
45
|
+
format('%.2<version>f', version: version % 10)
|
46
|
+
end
|
47
|
+
|
48
|
+
def handle_response(response)
|
49
|
+
return unless /\A#{Sip2::Responses::Status::RESPONSE_ID}/o.match?(response)
|
50
|
+
|
51
|
+
Sip2::Responses::Status.new response
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sip2
|
4
|
+
module Responses
|
5
|
+
#
|
6
|
+
# Sip2 Base response
|
7
|
+
#
|
8
|
+
class Base
|
9
|
+
attr_reader :raw_response
|
10
|
+
|
11
|
+
def initialize(raw_response)
|
12
|
+
@raw_response = raw_response
|
13
|
+
end
|
14
|
+
|
15
|
+
TIME_ZONE_LOOKUP_TABLE = {
|
16
|
+
'-12:00' => %w[Y],
|
17
|
+
'-11:00' => %w[X BST],
|
18
|
+
'-10:00' => %w[W HST BDT],
|
19
|
+
'-09:00' => %w[V YST HDT],
|
20
|
+
'-08:00' => %w[U PST YDT],
|
21
|
+
'-07:00' => %w[T MST PDT],
|
22
|
+
'-06:00' => %w[S CST MDT],
|
23
|
+
'-05:00' => %w[R EST CDT],
|
24
|
+
'-04:00' => %w[Q AST EDT],
|
25
|
+
'-03:00' => %w[P ADT],
|
26
|
+
'-02:00' => %w[O],
|
27
|
+
'-01:00' => %w[N],
|
28
|
+
'+00:00' => %w[Z GMT WET],
|
29
|
+
'+01:00' => %w[A CET BST],
|
30
|
+
'+02:00' => %w[B EET],
|
31
|
+
'+03:00' => %w[C],
|
32
|
+
'+04:00' => %w[D],
|
33
|
+
'+05:00' => %w[E],
|
34
|
+
'+06:00' => %w[F],
|
35
|
+
'+07:00' => %w[G],
|
36
|
+
'+08:00' => %w[H SST WST],
|
37
|
+
'+09:00' => %w[I JST],
|
38
|
+
'+10:00' => %w[K JDT],
|
39
|
+
'+11:00' => %w[L],
|
40
|
+
'+12:00' => %w[M NZST],
|
41
|
+
'+13:00' => %w[NZDT]
|
42
|
+
}.freeze
|
43
|
+
|
44
|
+
LANGUAGE_LOOKUP_TABLE = {
|
45
|
+
'000' => 'Unknown',
|
46
|
+
'001' => 'English',
|
47
|
+
'002' => 'French',
|
48
|
+
'003' => 'German',
|
49
|
+
'004' => 'Italian',
|
50
|
+
'005' => 'Dutch',
|
51
|
+
'006' => 'Swedish',
|
52
|
+
'007' => 'Finnish',
|
53
|
+
'008' => 'Spanish',
|
54
|
+
'009' => 'Danish',
|
55
|
+
'010' => 'Portuguese',
|
56
|
+
'011' => 'Canadian-French',
|
57
|
+
'012' => 'Norwegian',
|
58
|
+
'013' => 'Hebrew',
|
59
|
+
'014' => 'Japanese',
|
60
|
+
'015' => 'Russian',
|
61
|
+
'016' => 'Arabic',
|
62
|
+
'017' => 'Polish',
|
63
|
+
'018' => 'Greek',
|
64
|
+
'019' => 'Chinese',
|
65
|
+
'020' => 'Korean',
|
66
|
+
'021' => 'North American Spanish',
|
67
|
+
'022' => 'Tamil',
|
68
|
+
'023' => 'Malay',
|
69
|
+
'024' => 'United Kingdom',
|
70
|
+
'025' => 'Icelandic',
|
71
|
+
'026' => 'Belgian',
|
72
|
+
'027' => 'Taiwanese'
|
73
|
+
}.freeze
|
74
|
+
|
75
|
+
protected
|
76
|
+
|
77
|
+
DATE_MATCH_REGEX = '(\d{4})(\d{2})(\d{2})(.{4})(\d{2})(\d{2})(\d{2})'
|
78
|
+
|
79
|
+
def parse_fixed_response(position, count = 1)
|
80
|
+
raw_response[/\A#{self.class::RESPONSE_ID}.{#{position}}(.{#{count}})/, 1]
|
81
|
+
end
|
82
|
+
|
83
|
+
def parse_fixed_boolean(position)
|
84
|
+
parse_fixed_response(position) == 'Y'
|
85
|
+
end
|
86
|
+
|
87
|
+
def parse_optional_boolean(message_id)
|
88
|
+
raw_response[/(?:\A.{#{self.class::FIXED_LENGTH_CHARS}}|\|)#{message_id}([YN])\|/, 1] == 'Y'
|
89
|
+
end
|
90
|
+
|
91
|
+
def parse_text(message_id)
|
92
|
+
raw_response[/(?:\A.{#{self.class::FIXED_LENGTH_CHARS}}|\|)#{message_id}(.*?)\|/, 1]
|
93
|
+
end
|
94
|
+
|
95
|
+
def parse_datetime(position)
|
96
|
+
match = raw_response.match(/\A#{self.class::RESPONSE_ID}.{#{position}}#{DATE_MATCH_REGEX}/)
|
97
|
+
return unless match
|
98
|
+
|
99
|
+
_, year, month, day, zone, hour, minute, second = match.to_a
|
100
|
+
Time.new(
|
101
|
+
year.to_i, month.to_i, day.to_i,
|
102
|
+
hour.to_i, minute.to_i, second.to_i,
|
103
|
+
offset_from_zone(zone)
|
104
|
+
)
|
105
|
+
end
|
106
|
+
|
107
|
+
def offset_from_zone(zone)
|
108
|
+
zone.strip!
|
109
|
+
lookup = TIME_ZONE_LOOKUP_TABLE.find { |_, v| v.include? zone }
|
110
|
+
lookup ? lookup.first : '+00:00'
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
@@ -0,0 +1,144 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sip2
|
4
|
+
module Responses
|
5
|
+
#
|
6
|
+
# Sip2 Patron Information
|
7
|
+
#
|
8
|
+
# https://developers.exlibrisgroup.com/wp-content/uploads/2020/01/3M-Standard-Interchange-Protocol-Version-2.00.pdf
|
9
|
+
#
|
10
|
+
# Response message 64
|
11
|
+
# * patron status - 14 char, fixed-length required field
|
12
|
+
# * language - 3 char, fixed-length required field
|
13
|
+
# * transaction date - 18 char, fixed-length required field: YYYYMMDDZZZZHHMMSS
|
14
|
+
# * hold items count - 4 char, fixed-length required field
|
15
|
+
# * overdue items count - 4 char, fixed-length required field
|
16
|
+
# * charged items count - 4 char, fixed-length required field
|
17
|
+
# * fine items count - 4 char, fixed-length required field
|
18
|
+
# * recall items count - 4 char, fixed-length required field
|
19
|
+
# * unavailable holds count - 4 char, fixed-length required field
|
20
|
+
# * institution id - AO - variable-length required field
|
21
|
+
# * patron identifier - AA - variable-length required field
|
22
|
+
# * personal name - AE - variable-length required field
|
23
|
+
# * hold items limit - BZ - 4 char, fixed-length optional field
|
24
|
+
# * overdue items limit - CA - 4 char, fixed-length optional field
|
25
|
+
# * charged items limit - CB - 4 char, fixed-length optional field
|
26
|
+
# * valid patron - BL - 1 char, optional field: Y or N
|
27
|
+
# * valid patron password - CQ - 1 char, optional field: Y or N
|
28
|
+
# * currency type - BH - 3 char, fixed-length optional field
|
29
|
+
# * fee amount - BV - variable-length optional field
|
30
|
+
# * fee limit - CC - variable-length optional field
|
31
|
+
# * hold items - AS - variable-length optional field
|
32
|
+
# * overdue items - AT - variable-length optional field
|
33
|
+
# * charged items - AU - variable-length optional field
|
34
|
+
# * fine items - AV - variable-length optional field
|
35
|
+
# * recall items - BU - variable-length optional field
|
36
|
+
# * unavailable hold items - CD - variable-length optional field
|
37
|
+
# * home address - BD - variable-length optional field
|
38
|
+
# * email address - BE - variable-length optional field
|
39
|
+
# * home phone number - BF - variable-length optional field
|
40
|
+
# * screen message - AF - variable-length optional field
|
41
|
+
# * print line - AG - variable-length optional field
|
42
|
+
#
|
43
|
+
class PatronInformation < Base
|
44
|
+
RESPONSE_ID = 64
|
45
|
+
FIXED_LENGTH_CHARS = 61 # 59 chars + 2 for the header
|
46
|
+
|
47
|
+
def charge_privileges_denied?
|
48
|
+
parse_fixed_boolean 0
|
49
|
+
end
|
50
|
+
|
51
|
+
def renewal_privileges_denied?
|
52
|
+
parse_fixed_boolean 1
|
53
|
+
end
|
54
|
+
|
55
|
+
def recall_privileges_denied?
|
56
|
+
parse_fixed_boolean 2
|
57
|
+
end
|
58
|
+
|
59
|
+
def hold_privileges_denied?
|
60
|
+
parse_fixed_boolean 3
|
61
|
+
end
|
62
|
+
|
63
|
+
def card_reported_lost?
|
64
|
+
parse_fixed_boolean 4
|
65
|
+
end
|
66
|
+
|
67
|
+
def too_many_items_charged?
|
68
|
+
parse_fixed_boolean 5
|
69
|
+
end
|
70
|
+
|
71
|
+
def too_many_items_overdue?
|
72
|
+
parse_fixed_boolean 6
|
73
|
+
end
|
74
|
+
|
75
|
+
def too_many_renewals?
|
76
|
+
parse_fixed_boolean 7
|
77
|
+
end
|
78
|
+
|
79
|
+
def too_many_claims_of_items_returned?
|
80
|
+
parse_fixed_boolean 8
|
81
|
+
end
|
82
|
+
|
83
|
+
def too_many_items_lost?
|
84
|
+
parse_fixed_boolean 9
|
85
|
+
end
|
86
|
+
|
87
|
+
def excessive_outstanding_fines?
|
88
|
+
parse_fixed_boolean 10
|
89
|
+
end
|
90
|
+
|
91
|
+
def excessive_outstanding_fees?
|
92
|
+
parse_fixed_boolean 11
|
93
|
+
end
|
94
|
+
|
95
|
+
def recall_overdue?
|
96
|
+
parse_fixed_boolean 12
|
97
|
+
end
|
98
|
+
|
99
|
+
def too_many_items_billed?
|
100
|
+
parse_fixed_boolean 13
|
101
|
+
end
|
102
|
+
|
103
|
+
def language
|
104
|
+
LANGUAGE_LOOKUP_TABLE[parse_fixed_response(14, 3)]
|
105
|
+
end
|
106
|
+
|
107
|
+
def transaction_date
|
108
|
+
parse_datetime 17
|
109
|
+
end
|
110
|
+
|
111
|
+
def patron_valid?
|
112
|
+
parse_optional_boolean 'BL'
|
113
|
+
end
|
114
|
+
|
115
|
+
def authenticated?
|
116
|
+
parse_optional_boolean 'CQ'
|
117
|
+
end
|
118
|
+
|
119
|
+
def email
|
120
|
+
parse_text 'BE'
|
121
|
+
end
|
122
|
+
|
123
|
+
def location
|
124
|
+
parse_text 'AQ'
|
125
|
+
end
|
126
|
+
|
127
|
+
def screen_message
|
128
|
+
parse_text 'AF'
|
129
|
+
end
|
130
|
+
|
131
|
+
def inspect
|
132
|
+
format(
|
133
|
+
'#<%<class_name>s:0x%<object_id>p @patron_valid="%<patron_valid>s"' \
|
134
|
+
' @email="%<email>s" @authenticated="%<authenticated>s">',
|
135
|
+
class_name: self.class.name,
|
136
|
+
object_id: object_id,
|
137
|
+
patron_valid: patron_valid?,
|
138
|
+
email: email,
|
139
|
+
authenticated: authenticated?
|
140
|
+
)
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
@@ -0,0 +1,133 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sip2
|
4
|
+
module Responses
|
5
|
+
#
|
6
|
+
# Sip2 Patron Information
|
7
|
+
#
|
8
|
+
# https://developers.exlibrisgroup.com/wp-content/uploads/2020/01/3M-Standard-Interchange-Protocol-Version-2.00.pdf
|
9
|
+
#
|
10
|
+
# Response message 98
|
11
|
+
# * on-line status - 1 char, fixed-length required field: Y or N
|
12
|
+
# * checkin ok - 1 char, fixed-length required field: Y or N
|
13
|
+
# * checkout ok - 1 char, fixed-length required field: Y or N
|
14
|
+
# * ACS renewal policy - 1 char, fixed-length required field: Y or N
|
15
|
+
# * status update ok - 1 char, fixed-length required field: Y or N
|
16
|
+
# * off-line ok - 1 char, fixed-length required field: Y or N
|
17
|
+
# * timeout period - 3 char, fixed-length required field
|
18
|
+
# * retries allowed - 3 char, fixed-length required field
|
19
|
+
# * date / time sync - 18 char, fixed-length required field: YYYYMMDDZZZZHHMMSS
|
20
|
+
# * protocol version - 4 char, fixed-length required field: x.xx
|
21
|
+
# * institution ID - AO - variable-length required field
|
22
|
+
# * library name - AM - variable-length optional field
|
23
|
+
# * supported messages - BX - variable-length required field
|
24
|
+
# * terminal location - AN - variable-length optional field
|
25
|
+
# * screen message - AF - variable-length optional field
|
26
|
+
# * print line - AG - variable-length optional field
|
27
|
+
#
|
28
|
+
class Status < Base
|
29
|
+
RESPONSE_ID = 98
|
30
|
+
FIXED_LENGTH_CHARS = 36 # 34 chars + 2 for the header
|
31
|
+
SUPPORTED_MESSAGES = {
|
32
|
+
patron_status_request: 0,
|
33
|
+
checkout: 1,
|
34
|
+
checkin: 2,
|
35
|
+
block_patron: 3,
|
36
|
+
status: 4,
|
37
|
+
request_resend: 5,
|
38
|
+
login: 6,
|
39
|
+
patron_information: 7,
|
40
|
+
end_patron_session: 8,
|
41
|
+
fee_paid: 9,
|
42
|
+
item_information: 10,
|
43
|
+
item_status_update: 11,
|
44
|
+
patron_enable: 12,
|
45
|
+
hold: 13,
|
46
|
+
renew: 14,
|
47
|
+
renew_all: 15
|
48
|
+
}.freeze
|
49
|
+
|
50
|
+
def online?
|
51
|
+
parse_fixed_boolean 0
|
52
|
+
end
|
53
|
+
|
54
|
+
def checkin_ok?
|
55
|
+
parse_fixed_boolean 1
|
56
|
+
end
|
57
|
+
|
58
|
+
def checkout_ok?
|
59
|
+
parse_fixed_boolean 2
|
60
|
+
end
|
61
|
+
|
62
|
+
def acs_renewal_policy?
|
63
|
+
parse_fixed_boolean 3
|
64
|
+
end
|
65
|
+
|
66
|
+
def status_update_ok?
|
67
|
+
parse_fixed_boolean 4
|
68
|
+
end
|
69
|
+
|
70
|
+
def offline_ok?
|
71
|
+
parse_fixed_boolean 5
|
72
|
+
end
|
73
|
+
|
74
|
+
def timeout_period
|
75
|
+
timeout = parse_fixed_response 6, 3
|
76
|
+
timeout.to_i if timeout.match?(/\A\d+\z/)
|
77
|
+
end
|
78
|
+
|
79
|
+
def retries_allowed
|
80
|
+
retries = parse_fixed_response 9, 3
|
81
|
+
retries.to_i if retries.match?(/\A\d+\z/)
|
82
|
+
end
|
83
|
+
|
84
|
+
def date_sync
|
85
|
+
parse_datetime 12
|
86
|
+
end
|
87
|
+
|
88
|
+
def protocol_version
|
89
|
+
version = parse_fixed_response 30, 4
|
90
|
+
version.to_f if version.match?(/\A\d\.\d\d\z/)
|
91
|
+
end
|
92
|
+
|
93
|
+
def institution_id
|
94
|
+
parse_text 'AO'
|
95
|
+
end
|
96
|
+
|
97
|
+
def library_name
|
98
|
+
parse_text 'AM'
|
99
|
+
end
|
100
|
+
|
101
|
+
def supported_messages
|
102
|
+
message = parse_text('BX').to_s
|
103
|
+
|
104
|
+
SUPPORTED_MESSAGES.each_with_object([]) do |(supported_message, index), acc|
|
105
|
+
acc << supported_message if message[index] == 'Y'
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
def terminal_location
|
110
|
+
parse_text 'AN'
|
111
|
+
end
|
112
|
+
|
113
|
+
def screen_message
|
114
|
+
parse_text 'AF'
|
115
|
+
end
|
116
|
+
|
117
|
+
def print_line
|
118
|
+
parse_text 'AG'
|
119
|
+
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
format(
|
123
|
+
'#<%<class_name>s:0x%<object_id>p @online="%<online>s"' \
|
124
|
+
' @protocol_version="%<protocol_version>s"',
|
125
|
+
class_name: self.class.name,
|
126
|
+
object_id: object_id,
|
127
|
+
online: online?,
|
128
|
+
protocol_version: protocol_version
|
129
|
+
)
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
end
|
data/lib/sip2/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: sip2
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.2.
|
4
|
+
version: 0.2.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- abrom
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2020-11
|
11
|
+
date: 2020-12-11 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -30,14 +30,14 @@ dependencies:
|
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '13.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -58,14 +58,42 @@ dependencies:
|
|
58
58
|
requirements:
|
59
59
|
- - "~>"
|
60
60
|
- !ruby/object:Gem::Version
|
61
|
-
version: '0
|
61
|
+
version: '1.0'
|
62
62
|
type: :development
|
63
63
|
prerelease: false
|
64
64
|
version_requirements: !ruby/object:Gem::Requirement
|
65
65
|
requirements:
|
66
66
|
- - "~>"
|
67
67
|
- !ruby/object:Gem::Version
|
68
|
-
version: '0
|
68
|
+
version: '1.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: rubocop-performance
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '1.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '1.0'
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop-rspec
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '2.0'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '2.0'
|
69
97
|
- !ruby/object:Gem::Dependency
|
70
98
|
name: timecop
|
71
99
|
requirement: !ruby/object:Gem::Requirement
|
@@ -88,13 +116,18 @@ extensions: []
|
|
88
116
|
extra_rdoc_files: []
|
89
117
|
files:
|
90
118
|
- LICENSE
|
119
|
+
- README.md
|
91
120
|
- lib/sip2.rb
|
92
121
|
- lib/sip2/client.rb
|
93
122
|
- lib/sip2/connection.rb
|
123
|
+
- lib/sip2/messages/base.rb
|
94
124
|
- lib/sip2/messages/login.rb
|
95
125
|
- lib/sip2/messages/patron_information.rb
|
126
|
+
- lib/sip2/messages/status.rb
|
96
127
|
- lib/sip2/non_blocking_socket.rb
|
97
|
-
- lib/sip2/
|
128
|
+
- lib/sip2/responses/base.rb
|
129
|
+
- lib/sip2/responses/patron_information.rb
|
130
|
+
- lib/sip2/responses/status.rb
|
98
131
|
- lib/sip2/version.rb
|
99
132
|
homepage: https://github.com/Studiosity/sip2-ruby
|
100
133
|
licenses:
|
@@ -1,202 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Sip2
|
4
|
-
#
|
5
|
-
# Sip2 Patron Information
|
6
|
-
#
|
7
|
-
class PatronInformation
|
8
|
-
attr_reader :raw_response
|
9
|
-
|
10
|
-
def initialize(patron_response)
|
11
|
-
@raw_response = patron_response
|
12
|
-
end
|
13
|
-
|
14
|
-
def charge_privileges_denied?
|
15
|
-
parse_patron_status 0
|
16
|
-
end
|
17
|
-
|
18
|
-
def renewal_privileges_denied?
|
19
|
-
parse_patron_status 1
|
20
|
-
end
|
21
|
-
|
22
|
-
def recall_privileges_denied?
|
23
|
-
parse_patron_status 2
|
24
|
-
end
|
25
|
-
|
26
|
-
def hold_privileges_denied?
|
27
|
-
parse_patron_status 3
|
28
|
-
end
|
29
|
-
|
30
|
-
def card_reported_lost?
|
31
|
-
parse_patron_status 4
|
32
|
-
end
|
33
|
-
|
34
|
-
def too_many_items_charged?
|
35
|
-
parse_patron_status 5
|
36
|
-
end
|
37
|
-
|
38
|
-
def too_many_items_overdue?
|
39
|
-
parse_patron_status 6
|
40
|
-
end
|
41
|
-
|
42
|
-
def too_many_renewals?
|
43
|
-
parse_patron_status 7
|
44
|
-
end
|
45
|
-
|
46
|
-
def too_many_claims_of_items_returned?
|
47
|
-
parse_patron_status 8
|
48
|
-
end
|
49
|
-
|
50
|
-
def too_many_items_lost?
|
51
|
-
parse_patron_status 9
|
52
|
-
end
|
53
|
-
|
54
|
-
def excessive_outstanding_fines?
|
55
|
-
parse_patron_status 10
|
56
|
-
end
|
57
|
-
|
58
|
-
def excessive_outstanding_fees?
|
59
|
-
parse_patron_status 11
|
60
|
-
end
|
61
|
-
|
62
|
-
def recall_overdue?
|
63
|
-
parse_patron_status 12
|
64
|
-
end
|
65
|
-
|
66
|
-
def too_many_items_billed?
|
67
|
-
parse_patron_status 13
|
68
|
-
end
|
69
|
-
|
70
|
-
def language
|
71
|
-
LANGUAGE_LOOKUP_TABLE[parse_fixed_response(14, 3)]
|
72
|
-
end
|
73
|
-
|
74
|
-
def transaction_date
|
75
|
-
match = raw_response.match(/\A64.{17}(\d{4})(\d{2})(\d{2})(.{4})(\d{2})(\d{2})(\d{2})/)
|
76
|
-
return unless match
|
77
|
-
|
78
|
-
_, year, month, day, zone, hour, minute, second = match.to_a
|
79
|
-
Time.new(
|
80
|
-
year.to_i, month.to_i, day.to_i,
|
81
|
-
hour.to_i, minute.to_i, second.to_i,
|
82
|
-
offset_from_zone(zone)
|
83
|
-
)
|
84
|
-
end
|
85
|
-
|
86
|
-
def patron_valid?
|
87
|
-
parse_boolean 'BL'
|
88
|
-
end
|
89
|
-
|
90
|
-
def authenticated?
|
91
|
-
parse_boolean 'CQ'
|
92
|
-
end
|
93
|
-
|
94
|
-
def email
|
95
|
-
parse_text 'BE'
|
96
|
-
end
|
97
|
-
|
98
|
-
def location
|
99
|
-
parse_text 'AQ'
|
100
|
-
end
|
101
|
-
|
102
|
-
def screen_message
|
103
|
-
parse_text 'AF'
|
104
|
-
end
|
105
|
-
|
106
|
-
def inspect
|
107
|
-
format(
|
108
|
-
'#<%<class_name>s:0x%<object_id>p @patron_valid="%<patron_valid>s"' \
|
109
|
-
' @email="%<email>s" @authenticated="%<authenticated>s">',
|
110
|
-
class_name: self.class.name,
|
111
|
-
object_id: object_id,
|
112
|
-
patron_valid: patron_valid?,
|
113
|
-
email: email,
|
114
|
-
authenticated: authenticated?
|
115
|
-
)
|
116
|
-
end
|
117
|
-
|
118
|
-
private
|
119
|
-
|
120
|
-
def parse_boolean(message_id)
|
121
|
-
raw_response[/\|#{message_id}([YN])\|/, 1] == 'Y'
|
122
|
-
end
|
123
|
-
|
124
|
-
def parse_text(message_id)
|
125
|
-
raw_response[/\|#{message_id}(.*?)\|/, 1]
|
126
|
-
end
|
127
|
-
|
128
|
-
def parse_patron_status(position)
|
129
|
-
parse_fixed_response(position) == 'Y'
|
130
|
-
end
|
131
|
-
|
132
|
-
def parse_fixed_response(position, count = 1)
|
133
|
-
raw_response[/\A64.{#{position}}(.{#{count}})/, 1]
|
134
|
-
end
|
135
|
-
|
136
|
-
def offset_from_zone(zone)
|
137
|
-
zone.strip!
|
138
|
-
lookup = TIME_ZONE_LOOKUP_TABLE.find { |_, v| v.include? zone }
|
139
|
-
lookup ? lookup.first : '+00:00'
|
140
|
-
end
|
141
|
-
|
142
|
-
TIME_ZONE_LOOKUP_TABLE = {
|
143
|
-
'-12:00' => %w[Y],
|
144
|
-
'-11:00' => %w[X BST],
|
145
|
-
'-10:00' => %w[W HST BDT],
|
146
|
-
'-09:00' => %w[V YST HDT],
|
147
|
-
'-08:00' => %w[U PST YDT],
|
148
|
-
'-07:00' => %w[T MST PDT],
|
149
|
-
'-06:00' => %w[S CST MDT],
|
150
|
-
'-05:00' => %w[R EST CDT],
|
151
|
-
'-04:00' => %w[Q AST EDT],
|
152
|
-
'-03:00' => %w[P ADT],
|
153
|
-
'-02:00' => %w[O],
|
154
|
-
'-01:00' => %w[N],
|
155
|
-
'+00:00' => %w[Z GMT WET],
|
156
|
-
'+01:00' => %w[A CET BST],
|
157
|
-
'+02:00' => %w[B EET],
|
158
|
-
'+03:00' => %w[C],
|
159
|
-
'+04:00' => %w[D],
|
160
|
-
'+05:00' => %w[E],
|
161
|
-
'+06:00' => %w[F],
|
162
|
-
'+07:00' => %w[G],
|
163
|
-
'+08:00' => %w[H SST WST],
|
164
|
-
'+09:00' => %w[I JST],
|
165
|
-
'+10:00' => %w[K JDT],
|
166
|
-
'+11:00' => %w[L],
|
167
|
-
'+12:00' => %w[M NZST],
|
168
|
-
'+13:00' => %w[NZDT]
|
169
|
-
}.freeze
|
170
|
-
|
171
|
-
LANGUAGE_LOOKUP_TABLE = {
|
172
|
-
'000' => 'Unknown',
|
173
|
-
'001' => 'English',
|
174
|
-
'002' => 'French',
|
175
|
-
'003' => 'German',
|
176
|
-
'004' => 'Italian',
|
177
|
-
'005' => 'Dutch',
|
178
|
-
'006' => 'Swedish',
|
179
|
-
'007' => 'Finnish',
|
180
|
-
'008' => 'Spanish',
|
181
|
-
'009' => 'Danish',
|
182
|
-
'010' => 'Portuguese',
|
183
|
-
'011' => 'Canadian-French',
|
184
|
-
'012' => 'Norwegian',
|
185
|
-
'013' => 'Hebrew',
|
186
|
-
'014' => 'Japanese',
|
187
|
-
'015' => 'Russian',
|
188
|
-
'016' => 'Arabic',
|
189
|
-
'017' => 'Polish',
|
190
|
-
'018' => 'Greek',
|
191
|
-
'019' => 'Chinese',
|
192
|
-
'020' => 'Korean',
|
193
|
-
'021' => 'North American Spanish',
|
194
|
-
'022' => 'Tamil',
|
195
|
-
'023' => 'Malay',
|
196
|
-
'024' => 'United Kingdom',
|
197
|
-
'025' => 'Icelandic',
|
198
|
-
'026' => 'Belgian',
|
199
|
-
'027' => 'Taiwanese'
|
200
|
-
}.freeze
|
201
|
-
end
|
202
|
-
end
|