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.
@@ -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
data/lib/sip2/version.rb CHANGED
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module Sip2
2
- VERSION = '0.0.11'.freeze
4
+ VERSION = '0.2.3'
3
5
  end