sip2 0.0.11 → 0.2.3
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 +5 -5
- data/README.md +39 -0
- data/lib/sip2.rb +12 -1
- data/lib/sip2/client.rb +21 -5
- data/lib/sip2/connection.rb +41 -31
- data/lib/sip2/messages/base.rb +64 -0
- data/lib/sip2/messages/login.rb +23 -11
- data/lib/sip2/messages/patron_information.rb +22 -10
- data/lib/sip2/messages/status.rb +55 -0
- data/lib/sip2/non_blocking_socket.rb +5 -18
- 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 +3 -1
- metadata +66 -27
- data/.gitignore +0 -2
- data/.rubocop.yml +0 -9
- data/.travis.yml +0 -17
- data/Gemfile +0 -4
- data/Rakefile +0 -8
- data/lib/sip2/patron_information.rb +0 -199
- data/sip2.gemspec +0 -27
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
require 'socket'
|
2
4
|
require 'timeout'
|
3
5
|
|
@@ -8,22 +10,11 @@ module Sip2
|
|
8
10
|
#
|
9
11
|
class NonBlockingSocket < Socket
|
10
12
|
DEFAULT_TIMEOUT = 5
|
11
|
-
SEPARATOR = "\r".freeze
|
12
13
|
|
13
|
-
|
14
|
-
::Timeout.timeout (connection_timeout || DEFAULT_TIMEOUT), WriteTimeout do
|
15
|
-
send message + separator, 0
|
16
|
-
end
|
17
|
-
end
|
18
|
-
|
19
|
-
def gets_with_timeout(separator = SEPARATOR)
|
20
|
-
::Timeout.timeout (connection_timeout || DEFAULT_TIMEOUT), ReadTimeout do
|
21
|
-
gets separator
|
22
|
-
end
|
23
|
-
end
|
14
|
+
attr_accessor :connection_timeout
|
24
15
|
|
25
16
|
# rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
26
|
-
def self.connect(host
|
17
|
+
def self.connect(host:, port:, timeout: DEFAULT_TIMEOUT)
|
27
18
|
# Convert the passed host into structures the non-blocking calls can deal with
|
28
19
|
addr = Socket.getaddrinfo(host, nil)
|
29
20
|
sockaddr = Socket.pack_sockaddr_in(port, addr[0][3])
|
@@ -45,7 +36,7 @@ module Sip2
|
|
45
36
|
begin
|
46
37
|
# Verify there is now a good connection
|
47
38
|
socket.connect_nonblock(sockaddr)
|
48
|
-
rescue Errno::EISCONN
|
39
|
+
rescue Errno::EISCONN
|
49
40
|
# Good news everybody, the socket is connected!
|
50
41
|
rescue StandardError
|
51
42
|
# An unexpected exception was raised - the connection is no good.
|
@@ -62,9 +53,5 @@ module Sip2
|
|
62
53
|
end
|
63
54
|
end
|
64
55
|
# rubocop:enable Metrics/AbcSize, Metrics/MethodLength
|
65
|
-
|
66
|
-
private
|
67
|
-
|
68
|
-
attr_accessor :connection_timeout
|
69
56
|
end
|
70
57
|
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