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.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +136 -0
  4. data/Rakefile +10 -0
  5. data/bin/json-to-sip2 +109 -0
  6. data/bin/sip2-to-json +81 -0
  7. data/lib/sip2/checksum_encoder.rb +11 -0
  8. data/lib/sip2/field_parser_rules.rb +490 -0
  9. data/lib/sip2/fields.rb +742 -0
  10. data/lib/sip2/message/base_message.rb +135 -0
  11. data/lib/sip2/message/unknown_message.rb +26 -0
  12. data/lib/sip2/message.rb +95 -0
  13. data/lib/sip2/message_parser_rules.rb +122 -0
  14. data/lib/sip2/messages.rb +628 -0
  15. data/lib/sip2/parser.rb +14 -0
  16. data/lib/sip2/parser_atoms.rb +53 -0
  17. data/lib/sip2/transformer.rb +94 -0
  18. data/lib/sip2/types.rb +6 -0
  19. data/lib/sip2/version.rb +3 -0
  20. data/lib/sip2.rb +18 -0
  21. data/sip2-ruby.gemspec +39 -0
  22. data/test/fixture_helper.rb +31 -0
  23. data/test/fixtures/09.sip2 +1 -0
  24. data/test/fixtures/10.sip2 +1 -0
  25. data/test/fixtures/17.sip2 +1 -0
  26. data/test/fixtures/18.sip2 +1 -0
  27. data/test/fixtures/98-with-unexpected.sip2 +1 -0
  28. data/test/fixtures/98.sip2 +1 -0
  29. data/test/fixtures/99-with-unexpected.sip2 +1 -0
  30. data/test/fixtures/99.sip2 +1 -0
  31. data/test/fixtures/XX.sip2 +1 -0
  32. data/test/fixtures/alma/09.sip2 +1 -0
  33. data/test/fixtures/alma/10.sip2 +1 -0
  34. data/test/fixtures/alma/17.sip2 +1 -0
  35. data/test/fixtures/alma/18.sip2 +1 -0
  36. data/test/fixtures/alma/98.sip2 +1 -0
  37. data/test/fixtures/alma/99.sip2 +1 -0
  38. data/test/round_trip_spec.rb +82 -0
  39. data/test/sip2/message_parser_rules_spec.rb +50 -0
  40. data/test/sip2/parser_atoms_spec.rb +354 -0
  41. data/test/sip2/parser_test.rb +11 -0
  42. data/test/sip2_spec.rb +145 -0
  43. data/test/spec_helper.rb +4 -0
  44. data/test/test_helper.rb +4 -0
  45. 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
@@ -0,0 +1,6 @@
1
+ require 'dry-types'
2
+ module Sip2
3
+ module Types
4
+ include Dry.Types()
5
+ end
6
+ end
@@ -0,0 +1,3 @@
1
+ module Sip2
2
+ VERSION = "0.1.0"
3
+ end
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