sip2-ruby 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +136 -0
- data/Rakefile +10 -0
- data/bin/json-to-sip2 +109 -0
- data/bin/sip2-to-json +81 -0
- data/lib/sip2/checksum_encoder.rb +11 -0
- data/lib/sip2/field_parser_rules.rb +490 -0
- data/lib/sip2/fields.rb +742 -0
- data/lib/sip2/message/base_message.rb +135 -0
- data/lib/sip2/message/unknown_message.rb +26 -0
- data/lib/sip2/message.rb +95 -0
- data/lib/sip2/message_parser_rules.rb +122 -0
- data/lib/sip2/messages.rb +628 -0
- data/lib/sip2/parser.rb +14 -0
- data/lib/sip2/parser_atoms.rb +53 -0
- data/lib/sip2/transformer.rb +94 -0
- data/lib/sip2/types.rb +6 -0
- data/lib/sip2/version.rb +3 -0
- data/lib/sip2.rb +18 -0
- data/sip2-ruby.gemspec +39 -0
- data/test/fixture_helper.rb +31 -0
- data/test/fixtures/09.sip2 +1 -0
- data/test/fixtures/10.sip2 +1 -0
- data/test/fixtures/17.sip2 +1 -0
- data/test/fixtures/18.sip2 +1 -0
- data/test/fixtures/98-with-unexpected.sip2 +1 -0
- data/test/fixtures/98.sip2 +1 -0
- data/test/fixtures/99-with-unexpected.sip2 +1 -0
- data/test/fixtures/99.sip2 +1 -0
- data/test/fixtures/XX.sip2 +1 -0
- data/test/fixtures/alma/09.sip2 +1 -0
- data/test/fixtures/alma/10.sip2 +1 -0
- data/test/fixtures/alma/17.sip2 +1 -0
- data/test/fixtures/alma/18.sip2 +1 -0
- data/test/fixtures/alma/98.sip2 +1 -0
- data/test/fixtures/alma/99.sip2 +1 -0
- data/test/round_trip_spec.rb +82 -0
- data/test/sip2/message_parser_rules_spec.rb +50 -0
- data/test/sip2/parser_atoms_spec.rb +354 -0
- data/test/sip2/parser_test.rb +11 -0
- data/test/sip2_spec.rb +145 -0
- data/test/spec_helper.rb +4 -0
- data/test/test_helper.rb +4 -0
- metadata +190 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 35f09dc0ff1cc1cc6d033faf06eb5ec6108d37c4f9cd89d3e63ecf66614292ab
|
4
|
+
data.tar.gz: 6e21ab95f53d364cc215eeb66db6f540d0315cc203a355476d172d13bb7aaccd
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f695891e29b8ada0c7ec5fa68b606931fa5f0f4b866bd0e4cf02c1914dbe5a29013313384af73f76eae04f9b18d4078df03ddfbb9e558f48124a3c22b9edcc5b
|
7
|
+
data.tar.gz: 7ca77c9993bef861c547ef2405be6da9d0383cd3065be6464b582acfc50b9a13709e6227e288474e696c6481e3adddaf312b7bc814a15ce1328d6624fa76efa4
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2023 University of Borås
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
# Sip2 (sip2-ruby)
|
2
|
+
|
3
|
+
Sip2 is a library for parsing and encoding messages in the [Sip2 standard][sip2]
|
4
|
+
used for communication between library automation devices and automated
|
5
|
+
circulation systems. It is also two executables, `sip2-to-json` and
|
6
|
+
`json-to-sip2`, for parsing and encoding respectively.
|
7
|
+
|
8
|
+
[sip2]: https://developers.exlibrisgroup.com/wp-content/uploads/2020/01/3M-Standard-Interchange-Protocol-Version-2.00.pdf
|
9
|
+
|
10
|
+
Sip2 messages are parsed into a Hash/JSON representation, and the
|
11
|
+
Hash/JSON representation can be converted back into Sip2 format.
|
12
|
+
|
13
|
+
The library is only concerned with parsing and encoding and does not provide
|
14
|
+
anything related to the connection between the devices and the library system.
|
15
|
+
|
16
|
+
The parser is built on the excellent Parslet library.
|
17
|
+
|
18
|
+
## Basic usage
|
19
|
+
|
20
|
+
### As a script
|
21
|
+
|
22
|
+
Given that `sip2.log` contains only Sip2 messages, separated by `\r` (CR) and
|
23
|
+
optionally `\n` (LF).
|
24
|
+
|
25
|
+
```
|
26
|
+
sip2-to-json < sip2.log > sip2-log.json
|
27
|
+
|
28
|
+
json-to-sip2 < sip2-log.json > sip2.txt
|
29
|
+
```
|
30
|
+
|
31
|
+
Normally `sip2.log` and `sip2.txt` should now be semantically identical. (Field
|
32
|
+
order might differ.)
|
33
|
+
|
34
|
+
Typically `sip2-to-json` can be used to view, query and filter logs, while
|
35
|
+
`json-to-sip2` is best used in combination with `sip2-to-json` to rewrite
|
36
|
+
messages, operating on the JSON representation rather than on the raw strings:
|
37
|
+
|
38
|
+
```
|
39
|
+
sip2-to-json < sip2.log | your-script-here | json-to-sip2
|
40
|
+
```
|
41
|
+
|
42
|
+
### As a library
|
43
|
+
|
44
|
+
The main interfaces are two methods on the `Sip2` module: `Sip2.parse` and
|
45
|
+
`Sip2.encode`.
|
46
|
+
|
47
|
+
```ruby
|
48
|
+
require 'sip2'
|
49
|
+
message = "9900802.00\r"
|
50
|
+
|
51
|
+
parsed_message = Sip2.parse(message).first
|
52
|
+
#=> {:message_code=>"99", :message_name=>"SC Status", :status_code=>0,
|
53
|
+
# :max_print_width=>80, :protocol_version=>"2.00"}
|
54
|
+
|
55
|
+
parsed_message[:max_print_width] = 100
|
56
|
+
|
57
|
+
Sip2.encode(parsed_message)
|
58
|
+
#=> "9901002.00\r"
|
59
|
+
```
|
60
|
+
|
61
|
+
## Validation
|
62
|
+
|
63
|
+
`Sip2.parse` only accepts well formed messages, but handles invalid dates in a
|
64
|
+
possibly unexpected way. It enforces *date-like* values, but would accept e.g.
|
65
|
+
"2023-02-30" *and convert it* to 2023-03-02. Messages that are not well formed
|
66
|
+
will cause a `Parslet::ParseFailed` error to be raised. See [Parslet
|
67
|
+
documentation][parslet-docs] for how to get more information from the error.
|
68
|
+
|
69
|
+
[parslet-docs]: https://kschiess.github.io/parslet/documentation.html
|
70
|
+
|
71
|
+
`Sip2.encode` enforces the type of all values and outputs valid Sip2
|
72
|
+
messages. On most type errors, a `Dry::Struct::Error` will be raised.
|
73
|
+
|
74
|
+
Unrecognized message types and unrecognized fields for recognized messages are
|
75
|
+
accepted, in accordance with the Sip2 standard. The data is captured and can
|
76
|
+
round trip between parsing and encoding.
|
77
|
+
|
78
|
+
## Error detection and checksums
|
79
|
+
|
80
|
+
The Sip2 standard allows for some basic error detection using checksums. This
|
81
|
+
library does not validate checksums, but can create and update them.
|
82
|
+
When integrating with a system that validates and enforces checksums the way
|
83
|
+
checksums are calculated must match.
|
84
|
+
|
85
|
+
The standard is a bit vague on the topic of encodings, and especially for
|
86
|
+
checksums, where it is important since characters are "binary summed".
|
87
|
+
`Sip2#encode` accepts an optional `checksum_encoder`, a simple lambda that
|
88
|
+
accepts a string and returns a string. If supplied, the checksum will be
|
89
|
+
calculated on the return value of this lambda applied to the message. (The
|
90
|
+
lambda should not alter the original message.) In `Sip2::ChecksumEncoder` a
|
91
|
+
couple of encoders are provided (accessible both through their constant names
|
92
|
+
and through `#[]`):
|
93
|
+
|
94
|
+
* `ALMA`: Encodes the message as ASCII, using replacement characters for
|
95
|
+
non-ascii values. Matches the (undocumented) behaviour of Ex Libris Alma.
|
96
|
+
|
97
|
+
* `IDENTITY`: Pass through of the original message. Thus, a message in UTF-8
|
98
|
+
will be checksummed as UTF-8, while a message in Latin-1 will be checksummed
|
99
|
+
as Latin1. (This is the same as not specifying a `checksum_encoder`.)
|
100
|
+
|
101
|
+
The pre defined encoders are also available for `json-to-sip2` through the
|
102
|
+
command line option `--checksum-encoder`.
|
103
|
+
|
104
|
+
## Caveats
|
105
|
+
|
106
|
+
### Documentation
|
107
|
+
|
108
|
+
*Note that the ruby code is currently entirely undocumented.* This is due to
|
109
|
+
much of it being dynamically generated from a code representation of the Sip2
|
110
|
+
standard, and I simply do not know how to let rdoc or yard pick this up. The
|
111
|
+
parts that are static can of course be documented, but as they read or return
|
112
|
+
the dynamically generated message classes the documentation would still be
|
113
|
+
incomplete.
|
114
|
+
|
115
|
+
Running `script/sip2-messages-info` will output a JSON representation of all
|
116
|
+
known messages and the fields they include. There is also a JSON representation
|
117
|
+
of all fields in `doc/sip2_fields.json`. These two sources can be used to some
|
118
|
+
extent to see how the mapping between the Sip2 format and the Hash/JSON
|
119
|
+
representation looks like.
|
120
|
+
|
121
|
+
### Time Zones
|
122
|
+
|
123
|
+
Time zones are not fully implemented and do not round trip. The Sip2
|
124
|
+
specification declares that time zones should be expressed according to ancient
|
125
|
+
ANSI standard X3.43, which in its turn refers to X3.51. This standard is
|
126
|
+
mentioned in RFC822, which specifies a few two or three letter codes, "military"
|
127
|
+
one letter time zone codes, and explicit offset using four digits prefixed with
|
128
|
+
`+` or `-`. The prefixed digits will not fit in the designated four character
|
129
|
+
segment.
|
130
|
+
|
131
|
+
In this library only `Z` for "UTC" and ` ` (four spaces, no time zone) are
|
132
|
+
round tripped. When parsing, `Z` and `UTC` will be parsed as times with UTC time
|
133
|
+
zone, four blanks will be parsed as local time, and military one letter time
|
134
|
+
zone codes will be parsed into times with the correct offset but with no
|
135
|
+
`Time#zone` set. When encoding, times in UTC will be encoded as `Z`, and all
|
136
|
+
other times, including local time, will be encoded without time zone data.
|
data/Rakefile
ADDED
data/bin/json-to-sip2
ADDED
@@ -0,0 +1,109 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../environment'
|
4
|
+
|
5
|
+
require 'yajl'
|
6
|
+
require 'optparse'
|
7
|
+
require 'sip2'
|
8
|
+
require 'sip2/checksum_encoder'
|
9
|
+
|
10
|
+
options = {
|
11
|
+
checksum_encoder: nil,
|
12
|
+
continue_on_error: false,
|
13
|
+
debug: false,
|
14
|
+
error_detection: false,
|
15
|
+
line_by_line: true,
|
16
|
+
strip_extra_fields: false,
|
17
|
+
}
|
18
|
+
OptionParser.new do |opts|
|
19
|
+
opts.banner = "Usage: #{$0} [options]"
|
20
|
+
opts.separator ""
|
21
|
+
opts.separator "Options:"
|
22
|
+
|
23
|
+
opts.on("-a", "--array", "Read messages from a JSON array.") do
|
24
|
+
options[:line_by_line] = false
|
25
|
+
end
|
26
|
+
|
27
|
+
opts.on("-c", "--continue-on-error",
|
28
|
+
"When parsing an object fails, continue with the next.",
|
29
|
+
"The exit code will still be non-zero.",
|
30
|
+
'This option does not apply when using `--array`.'
|
31
|
+
) do
|
32
|
+
options[:continue_on_error] = true
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-C", "--checksum-encoder=<NAME>",
|
36
|
+
"Reencode the message with Sip2::ChecksumEncoder::<NAME>",
|
37
|
+
"before calculating the checksum, where <NAME> is one of:",
|
38
|
+
*Sip2::ChecksumEncoder.constants.map { |c| sprintf(" %s", c) }
|
39
|
+
) do |name|
|
40
|
+
options[:checksum_encoder] = name
|
41
|
+
end
|
42
|
+
|
43
|
+
opts.on("-E", "--error-detection",
|
44
|
+
"Append error detection fields to the message",
|
45
|
+
) do |name|
|
46
|
+
options[:error_detection] = true
|
47
|
+
end
|
48
|
+
|
49
|
+
opts.on("--strip-extra-fields",
|
50
|
+
"Remove extra fields that are not part of the specification.",
|
51
|
+
) do
|
52
|
+
options[:strip_extra_fields] = true
|
53
|
+
end
|
54
|
+
|
55
|
+
opts.on("-v", "--version", "Print version and exit.") do
|
56
|
+
puts Sip2::VERSION
|
57
|
+
exit
|
58
|
+
end
|
59
|
+
|
60
|
+
opts.on("-h", "--help", "Display this message.") do
|
61
|
+
puts opts
|
62
|
+
exit
|
63
|
+
end
|
64
|
+
|
65
|
+
end.parse!
|
66
|
+
|
67
|
+
UnknownError = Class.new(StandardError)
|
68
|
+
|
69
|
+
checksum_encoder_name = options[:checksum_encoder] || ENV['SIP2_CHECKSUM_ENCODER']
|
70
|
+
checksum_encoder = Sip2::ChecksumEncoder[checksum_encoder_name] if checksum_encoder_name
|
71
|
+
|
72
|
+
encode = ->(obj) {
|
73
|
+
Sip2.encode(
|
74
|
+
obj,
|
75
|
+
add_error_detection: options[:error_detection],
|
76
|
+
checksum_encoder: checksum_encoder,
|
77
|
+
strip_extra_fields: options[:strip_extra_fields]
|
78
|
+
)
|
79
|
+
}
|
80
|
+
|
81
|
+
if options[:line_by_line]
|
82
|
+
error_count = 0
|
83
|
+
Yajl.load($stdin) do |obj|
|
84
|
+
begin
|
85
|
+
puts encode.call(obj)
|
86
|
+
rescue Dry::Struct::Error => error
|
87
|
+
warn "Invalid object: #{Yajl.dump(obj)}"
|
88
|
+
if options[:continue_on_error]
|
89
|
+
warn error.message
|
90
|
+
error_count += 1
|
91
|
+
else
|
92
|
+
raise error
|
93
|
+
end
|
94
|
+
end
|
95
|
+
end
|
96
|
+
if error_count > 0
|
97
|
+
warn sprintf(
|
98
|
+
"WARNING: %d object%s could not be parsed",
|
99
|
+
error_count,
|
100
|
+
error_count == 1 ? "" : "s"
|
101
|
+
)
|
102
|
+
exit 1
|
103
|
+
end
|
104
|
+
else
|
105
|
+
data = Yajl.load($stdin)
|
106
|
+
data.each do |obj|
|
107
|
+
puts encode.call(obj)
|
108
|
+
end
|
109
|
+
end
|
data/bin/sip2-to-json
ADDED
@@ -0,0 +1,81 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require_relative '../environment'
|
4
|
+
|
5
|
+
require 'json'
|
6
|
+
require 'optparse'
|
7
|
+
require 'sip2'
|
8
|
+
|
9
|
+
options = {
|
10
|
+
continue_on_error: false,
|
11
|
+
debug: false,
|
12
|
+
line_by_line: true,
|
13
|
+
}
|
14
|
+
OptionParser.new do |opts|
|
15
|
+
opts.banner = "Usage: #{$0} [options]"
|
16
|
+
opts.separator ""
|
17
|
+
opts.separator "Options:"
|
18
|
+
|
19
|
+
opts.on("-a", "--all", "Parse all messages into a JSON array.") do
|
20
|
+
options[:line_by_line] = false
|
21
|
+
end
|
22
|
+
|
23
|
+
opts.on("-c", "--continue-on-error",
|
24
|
+
"When parsing a line fails, continue with the next.",
|
25
|
+
"The exit code will still be non-zero.",
|
26
|
+
'This option does not apply when using `--all`.'
|
27
|
+
) do
|
28
|
+
options[:continue_on_error] = true
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("-D", "--debug", "Report errors with a parse tree.") do
|
32
|
+
options[:debug] = true
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("-v", "--version", "Print version and exit.") do
|
36
|
+
puts Sip2::VERSION
|
37
|
+
exit
|
38
|
+
end
|
39
|
+
|
40
|
+
opts.on("-h", "--help", "Display this message.") do
|
41
|
+
puts opts
|
42
|
+
exit
|
43
|
+
end
|
44
|
+
|
45
|
+
end.parse!
|
46
|
+
|
47
|
+
if options[:line_by_line]
|
48
|
+
begin
|
49
|
+
error_count = 0
|
50
|
+
ARGF.each_line.with_index(1) do |data,i|
|
51
|
+
begin
|
52
|
+
puts Sip2.parse(data).first.to_json
|
53
|
+
rescue Parslet::ParseFailed => error
|
54
|
+
warn "Error parsing line #{i}: #{data.dump}"
|
55
|
+
warn error.parse_failure_cause.ascii_tree if options[:debug]
|
56
|
+
if options[:continue_on_error]
|
57
|
+
error_count += 1
|
58
|
+
else
|
59
|
+
raise error
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
if error_count > 0
|
64
|
+
warn sprintf(
|
65
|
+
"WARNING: %d line%s could not be parsed",
|
66
|
+
error_count,
|
67
|
+
error_count == 1 ? "" : "s"
|
68
|
+
)
|
69
|
+
exit 1
|
70
|
+
end
|
71
|
+
rescue Errno::EPIPE
|
72
|
+
exit 0
|
73
|
+
end
|
74
|
+
else
|
75
|
+
begin
|
76
|
+
puts Sip2.parse(ARGF.read).to_json
|
77
|
+
rescue Parslet::ParseFailed => error
|
78
|
+
warn error.parse_failure_cause.ascii_tree if options[:debug]
|
79
|
+
raise error
|
80
|
+
end
|
81
|
+
end
|