sip2 0.2.1 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
+
[![Travis Build Status](http://img.shields.io/travis/Studiosity/sip2-ruby.svg?style=flat)](https://travis-ci.org/Studiosity/sip2-ruby)
|
2
|
+
[![Gem Version](http://img.shields.io/gem/v/sip2.svg?style=flat)](#)
|
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
|