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
@@ -0,0 +1,94 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
require 'parslet/convenience'
|
3
|
+
|
4
|
+
require 'sip2/messages'
|
5
|
+
module Sip2
|
6
|
+
|
7
|
+
class Transformer < Parslet::Transform
|
8
|
+
|
9
|
+
rule(nil: simple(:_)) { nil }
|
10
|
+
rule(empty_hash: simple(:_)) { {} }
|
11
|
+
|
12
|
+
rule(int: simple(:x)) { Integer(x) }
|
13
|
+
|
14
|
+
rule(int: sequence(:x)) { x.empty? ? 0 : x }
|
15
|
+
|
16
|
+
rule(str: simple(:x)) { String(x) }
|
17
|
+
|
18
|
+
rule(str: sequence(:x)) { x.empty? ? "" : x }
|
19
|
+
|
20
|
+
rule(bool: simple(:x)) {
|
21
|
+
case x
|
22
|
+
when "Y", "1"
|
23
|
+
true
|
24
|
+
when "U"
|
25
|
+
nil
|
26
|
+
else
|
27
|
+
false
|
28
|
+
end
|
29
|
+
}
|
30
|
+
|
31
|
+
|
32
|
+
rule(tz: simple(:x)) {
|
33
|
+
tz = String(x).strip
|
34
|
+
tz unless tz.empty?
|
35
|
+
}
|
36
|
+
|
37
|
+
rule(
|
38
|
+
year: simple(:year),
|
39
|
+
month: simple(:month),
|
40
|
+
day: simple(:day),
|
41
|
+
zone: simple(:zone),
|
42
|
+
hour: simple(:hour),
|
43
|
+
minute: simple(:minute),
|
44
|
+
second: simple(:second)
|
45
|
+
) {
|
46
|
+
Time.new(year, month, day, hour, minute, second, zone)
|
47
|
+
}
|
48
|
+
|
49
|
+
rule(message_code: simple(:c), message_data: simple(:d)) {
|
50
|
+
{
|
51
|
+
message_code: c,
|
52
|
+
message_name: "Unknown Message",
|
53
|
+
message_data: d,
|
54
|
+
}
|
55
|
+
}
|
56
|
+
|
57
|
+
rule(message_code: simple(:x)) {
|
58
|
+
message_type = MESSAGES_BY_CODE.fetch(x)
|
59
|
+
{
|
60
|
+
message_code: x,
|
61
|
+
message_name: message_type.fetch(:name),
|
62
|
+
}
|
63
|
+
}
|
64
|
+
|
65
|
+
rule(merge_repeat: subtree(:x)) {
|
66
|
+
x.each_with_object({}) { |el, hsh|
|
67
|
+
if el.key?(:merge_repeat_to_array)
|
68
|
+
real = el.fetch(:merge_repeat_to_array)
|
69
|
+
real.map { |k,v|
|
70
|
+
hsh.fetch(k) { hsh[k] = [] } << v
|
71
|
+
}
|
72
|
+
else
|
73
|
+
hsh.merge!(el)
|
74
|
+
end
|
75
|
+
}
|
76
|
+
}
|
77
|
+
|
78
|
+
%i[hold_items overdue_items charged_items fine_items recall_items unavailable_hold_items].each do |sym|
|
79
|
+
rule(sym => simple(:x)) { x }
|
80
|
+
end
|
81
|
+
|
82
|
+
rule(
|
83
|
+
message_identifiers: subtree(:m),
|
84
|
+
ordered_fields: subtree(:o),
|
85
|
+
delimited_fields: subtree(:d),
|
86
|
+
error_detection: subtree(:e)
|
87
|
+
) {
|
88
|
+
[m, o, d, e].reduce { |res,el| res.merge(el) }
|
89
|
+
}
|
90
|
+
|
91
|
+
|
92
|
+
end
|
93
|
+
|
94
|
+
end
|
data/lib/sip2/types.rb
ADDED
data/lib/sip2/version.rb
ADDED
data/lib/sip2.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'sip2/version'
|
2
|
+
require 'sip2/parser'
|
3
|
+
require 'sip2/transformer'
|
4
|
+
require 'sip2/message'
|
5
|
+
|
6
|
+
module Sip2
|
7
|
+
PARSER = Sip2::Parser.new
|
8
|
+
TRANSFORMER = Sip2::Transformer.new
|
9
|
+
|
10
|
+
def self.parse(data)
|
11
|
+
TRANSFORMER.apply(PARSER.parse(data))
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.encode(hsh, **encoding_options)
|
15
|
+
Sip2::Message.from_hash(hsh).encode(**encoding_options)
|
16
|
+
end
|
17
|
+
|
18
|
+
end
|
data/sip2-ruby.gemspec
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
require_relative 'lib/sip2/version'
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "sip2-ruby"
|
5
|
+
spec.version = Sip2::VERSION
|
6
|
+
spec.authors = ["Daniel Sandbecker"]
|
7
|
+
spec.email = ["daniel.sandbecker@hb.se"]
|
8
|
+
|
9
|
+
spec.summary = "Parse and encode SIP2 messages"
|
10
|
+
spec.description = <<-EOS
|
11
|
+
A library and executables for parsing and encoding SIP2 messages (a standard
|
12
|
+
for data exchange between Library Automation Systems and Library Systems).
|
13
|
+
EOS
|
14
|
+
spec.homepage = "https://github.com/ub-library/sip2-ruby"
|
15
|
+
spec.license = "MIT"
|
16
|
+
spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
|
17
|
+
|
18
|
+
spec.metadata["homepage_uri"] = spec.homepage
|
19
|
+
spec.metadata["source_code_uri"] = spec.homepage
|
20
|
+
|
21
|
+
spec.files = Dir.glob("{lib,test}/**/*") + %w[
|
22
|
+
LICENSE.txt
|
23
|
+
Rakefile
|
24
|
+
README.md
|
25
|
+
sip2-ruby.gemspec
|
26
|
+
]
|
27
|
+
spec.bindir = "bin"
|
28
|
+
spec.executables = %w[sip2-to-json json-to-sip2]
|
29
|
+
spec.require_paths = %w[lib]
|
30
|
+
|
31
|
+
spec.add_runtime_dependency "dry-struct", "~> 1.4"
|
32
|
+
spec.add_runtime_dependency "parslet", "~> 2.0"
|
33
|
+
spec.add_runtime_dependency "yajl-ruby", "~> 1.4"
|
34
|
+
|
35
|
+
spec.add_development_dependency "minitest", "~> 5.19"
|
36
|
+
spec.add_development_dependency "minitest-reporters", "~> 1.6"
|
37
|
+
spec.add_development_dependency "pry"
|
38
|
+
spec.add_development_dependency "rake"
|
39
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module FixtureHelper
|
2
|
+
|
3
|
+
FIXTURE_DIR = File.join(__dir__, "fixtures")
|
4
|
+
|
5
|
+
KNOWN_MESSAGES_FIXTURE_CODES = %w[
|
6
|
+
09
|
7
|
+
10
|
8
|
+
17
|
9
|
+
18
|
10
|
+
98
|
11
|
+
99
|
12
|
+
]
|
13
|
+
|
14
|
+
UNKNOWN_MESSAGES_FIXTURE_CODES = %w[
|
15
|
+
XX
|
16
|
+
]
|
17
|
+
|
18
|
+
MESSAGES_WITH_EXTRA_FIELDS_FIXTURE_CODES = %w[
|
19
|
+
98-with-unexpected
|
20
|
+
99-with-unexpected
|
21
|
+
]
|
22
|
+
|
23
|
+
def sip2_fixture_path(code)
|
24
|
+
File.join(FIXTURE_DIR, "#{code}.sip2")
|
25
|
+
end
|
26
|
+
|
27
|
+
def sip2_fixture(code)
|
28
|
+
File.read(sip2_fixture_path(code))
|
29
|
+
end
|
30
|
+
|
31
|
+
end
|
@@ -0,0 +1 @@
|
|
1
|
+
09N20230822 15234020230822 152340APSomeLocation|AO|ABSomeBarcode|AC|CH|AY1AZE92F
|
@@ -0,0 +1 @@
|
|
1
|
+
101YNN20230822 152248AO|AB16000000210799|AQSomeLocation|AJSome example title by Some Author|CL5|CK001|AY1AZDF1B
|
@@ -0,0 +1 @@
|
|
1
|
+
1720230822 152339AO|ABSomeBarcode|AC|AY1AZF399
|
@@ -0,0 +1 @@
|
|
1
|
+
1804000120230822 110550ABSomeBarcode|AJSome example title by Some Author|AH2023-08-31 23:59:00 CEST|BGThe Library|BHSEK|CK001|AQSomeOtherLocation|APSomeLocation|AY1AZCB01
|
@@ -0,0 +1 @@
|
|
1
|
+
98YYNNNY00500520230822 1524342.00AO3662|ZZSomeUnexpectedFieldValue|BXYYYNYNYYYYYNNNYY|AMThe Library|ANSomeLocation|XZAnohtherExtraField|AY1AZD0B7
|
@@ -0,0 +1 @@
|
|
1
|
+
98YYNNNY00500520230822 1524342.00AO3662|BXYYYNYNYYYYYNNNYY|AMThe Library|ANSomeLocation|AY1AZE3C0
|
@@ -0,0 +1 @@
|
|
1
|
+
9900802.00XXSomeUnexpectedValue|AY1AZF3CE
|
@@ -0,0 +1 @@
|
|
1
|
+
9900802.00AY1AZFCA0
|
@@ -0,0 +1 @@
|
|
1
|
+
XXSomeUnknownData
|
@@ -0,0 +1 @@
|
|
1
|
+
09N20230822 15234020230822 152340APLanedisk|AO|AB16000000210799|AC|CH|AY1AZEC52
|
@@ -0,0 +1 @@
|
|
1
|
+
101YNN20230822 152248AB16000000210799|AJInternational financial reporting standards : as issued at 1 January 2014.|AO|AQPlan 3 (Kan inte lånas hem)|CK001|CL5|AY1AZCD21
|
@@ -0,0 +1 @@
|
|
1
|
+
1720230822 152339AO|AB16000000210799|AC|AY1AZF51A
|
@@ -0,0 +1 @@
|
|
1
|
+
1803000120230822 152248AB16000000210799|AJInternational financial reporting standards : as issued at 1 January 2014.|APÅterlämningsrummet|AQPlan 3 (Kan inte lånas hem)|BGThe Library|BHSEK|CK001|AY1AZBFFB
|
@@ -0,0 +1 @@
|
|
1
|
+
98YYNNNY00500520230822 1524342.00AMThe Library|ANÅterlämningsrummet|AO3662|BXYYYNYNYYYYYNNNYY|AY1AZE132
|
@@ -0,0 +1 @@
|
|
1
|
+
9900802.00AY1AZFCA0
|
@@ -0,0 +1,82 @@
|
|
1
|
+
require_relative 'spec_helper'
|
2
|
+
require_relative 'fixture_helper'
|
3
|
+
|
4
|
+
include FixtureHelper
|
5
|
+
|
6
|
+
require 'sip2'
|
7
|
+
|
8
|
+
describe Sip2 do
|
9
|
+
|
10
|
+
# Note:
|
11
|
+
#
|
12
|
+
# This libraray aims to round trip all data in Sip2 messages to and from JSON
|
13
|
+
# (or a hash representation). But since delimited fields can come in any
|
14
|
+
# order, two Sip2 representations of the exact same data does not need to be
|
15
|
+
# identical. This library *makes no attempt at preserving the order* but just
|
16
|
+
# all data.
|
17
|
+
#
|
18
|
+
# But this makes it hard to verify that the round trip actually works. All we
|
19
|
+
# can compare are the strings, with possibly different order of the fields.
|
20
|
+
# And even if a Sip2 message string initially written by this library might
|
21
|
+
# round trip identically through parse >> encode, that is an implementation
|
22
|
+
# detail and not guaranteed.
|
23
|
+
#
|
24
|
+
# Since ruby doesn't consider key order for hashes when comparing for
|
25
|
+
# equality, round trips from the hash representation through `encode >> parse`
|
26
|
+
# *can* be verified, so this is what we specify.
|
27
|
+
#
|
28
|
+
# But our fixtures are Sip2 messages, so we actually round trip twice, `parse
|
29
|
+
# >> encode >> parse`, but we only verify the last roundtrip.
|
30
|
+
#
|
31
|
+
describe "round trip" do
|
32
|
+
describe "Known messages" do
|
33
|
+
KNOWN_MESSAGES_FIXTURE_CODES.each do |code|
|
34
|
+
|
35
|
+
describe code do
|
36
|
+
|
37
|
+
msg = sip2_fixture(code)
|
38
|
+
|
39
|
+
it "round trips through (encode >> parse)" do
|
40
|
+
hsh = Sip2.parse(msg).first
|
41
|
+
|
42
|
+
assert_equal hsh, Sip2.parse(Sip2.encode(hsh)).first
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "Unknown messages" do
|
49
|
+
UNKNOWN_MESSAGES_FIXTURE_CODES.each do |code|
|
50
|
+
|
51
|
+
describe code do
|
52
|
+
|
53
|
+
msg = sip2_fixture(code)
|
54
|
+
|
55
|
+
it "round trips through (encode >> parse)" do
|
56
|
+
hsh = Sip2.parse(msg).first
|
57
|
+
|
58
|
+
assert_equal hsh, Sip2.parse(Sip2.encode(hsh)).first
|
59
|
+
end
|
60
|
+
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
describe "Known messages with unexpected fields" do
|
66
|
+
MESSAGES_WITH_EXTRA_FIELDS_FIXTURE_CODES.each do |code|
|
67
|
+
describe code do
|
68
|
+
|
69
|
+
msg = sip2_fixture(code)
|
70
|
+
|
71
|
+
it "round trips through (encode >> parse)" do
|
72
|
+
hsh = Sip2.parse(msg).first
|
73
|
+
|
74
|
+
assert_equal hsh, Sip2.parse(Sip2.encode(hsh)).first
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative '../spec_helper'
|
2
|
+
require 'sip2/message_parser_rules'
|
3
|
+
|
4
|
+
include Sip2
|
5
|
+
|
6
|
+
class ExampleParser
|
7
|
+
include Sip2::MessageParserRules
|
8
|
+
end
|
9
|
+
|
10
|
+
describe MessageParserRules do
|
11
|
+
|
12
|
+
let(:parser) { ExampleParser.new }
|
13
|
+
|
14
|
+
describe "sc_status" do
|
15
|
+
it "accepts a 99 SC Status message" do
|
16
|
+
parser.sc_status.parse("9900302.00\r")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "doesn't accept garbage" do
|
20
|
+
assert_raises(Parslet::ParseFailed) {
|
21
|
+
parser.sc_status.parse("abcd")
|
22
|
+
}
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
describe(:unknown_message) do
|
28
|
+
it "accepts any message with a not known identifier" do
|
29
|
+
["XYfoo", "xybar", "77"].each do |msg|
|
30
|
+
parser.unknown_message.parse(sprintf("%s\r", msg))
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
it "doesn't accept messages with known identifiers" do
|
35
|
+
["10foo", "98bar", "99"].each do |msg|
|
36
|
+
assert_raises(Parslet::ParseFailed) {
|
37
|
+
parser.unknown_message.parse(sprintf("%s\r", msg))
|
38
|
+
}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
it "doesn't accept messages with invalid identifiers" do
|
43
|
+
["", "X", "!Y"].each do |msg|
|
44
|
+
assert_raises(Parslet::ParseFailed) {
|
45
|
+
parser.unknown_message.parse(sprintf("%s\r", msg))
|
46
|
+
}
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|