sip2 0.0.10 → 0.2.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/README.md +39 -0
- data/lib/sip2.rb +9 -1
- data/lib/sip2/client.rb +21 -5
- data/lib/sip2/connection.rb +38 -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 +52 -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