sip2 0.0.10 → 0.2.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
- def send_with_timeout(message, separator = SEPARATOR)
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, port, timeout = DEFAULT_TIMEOUT)
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 # rubocop:disable Lint/HandleExceptions
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
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
- VERSION = '0.0.10'.freeze
4
+ VERSION = '0.2.2'
3
5
  end