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,135 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
require 'sip2/types'
|
3
|
+
require 'sip2/fields'
|
4
|
+
module Sip2
|
5
|
+
|
6
|
+
module Message
|
7
|
+
class BaseMessage < Dry::Struct
|
8
|
+
|
9
|
+
SEQUENCE_NUMBER_FORMAT = Sip2::FIELDS.fetch(:sequence_number).fetch(:format)
|
10
|
+
CHECKSUM_CODE = Sip2::FIELDS.fetch(:checksum).fetch(:code)
|
11
|
+
|
12
|
+
schema schema.strict
|
13
|
+
|
14
|
+
transform_keys(&:to_sym)
|
15
|
+
|
16
|
+
class << self
|
17
|
+
|
18
|
+
def ordered_fields
|
19
|
+
@ordered_fields
|
20
|
+
end
|
21
|
+
|
22
|
+
def delimited_fields
|
23
|
+
@delimited_fields ||=
|
24
|
+
(@required_delimited_fields + @optional_delimited_fields)
|
25
|
+
.sort_by { |field| Sip2::FIELDS[field][:code] }
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
|
30
|
+
def fields
|
31
|
+
self.class.fields
|
32
|
+
end
|
33
|
+
|
34
|
+
def ordered_fields
|
35
|
+
self.class.ordered_fields
|
36
|
+
end
|
37
|
+
|
38
|
+
def delimited_fields
|
39
|
+
self.class.delimited_fields
|
40
|
+
end
|
41
|
+
|
42
|
+
def format_field(field_name)
|
43
|
+
field_info = Sip2::FIELDS.fetch(field_name)
|
44
|
+
attribute_name = field_info.fetch(:name, field_name)
|
45
|
+
format = field_info.fetch(:format)
|
46
|
+
|
47
|
+
if self.attributes.key?(attribute_name)
|
48
|
+
format.call(self[attribute_name])
|
49
|
+
else
|
50
|
+
""
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def checksum(msg)
|
55
|
+
# Add each character as an unsigned binary number
|
56
|
+
sum = msg.codepoints.sum
|
57
|
+
|
58
|
+
# Take the lower 16 bits of the total
|
59
|
+
sum16 = sum & 0xFFFF
|
60
|
+
|
61
|
+
# Perform a 2's complement
|
62
|
+
comp2 = (sum16 ^ 0xFFFF) + 1
|
63
|
+
|
64
|
+
# The checksum field is the result represented by four hex digits
|
65
|
+
sprintf("%04X", comp2)
|
66
|
+
end
|
67
|
+
|
68
|
+
def checksum_field(message, checksum_encoder: nil)
|
69
|
+
msg =
|
70
|
+
if checksum_encoder
|
71
|
+
checksum_encoder.call(message)
|
72
|
+
else
|
73
|
+
message
|
74
|
+
end
|
75
|
+
CHECKSUM_CODE + checksum(msg + CHECKSUM_CODE)
|
76
|
+
end
|
77
|
+
|
78
|
+
def sequence_number_field
|
79
|
+
if attributes.has_key?(:sequence_number)
|
80
|
+
SEQUENCE_NUMBER_FORMAT.call(self[:sequence_number])
|
81
|
+
else
|
82
|
+
""
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def error_detection_fields(message, **checksum_options)
|
87
|
+
if attributes.has_key?(:checksum)
|
88
|
+
sequence_number = sequence_number_field
|
89
|
+
sequence_number + checksum_field(message + sequence_number, **checksum_options)
|
90
|
+
else
|
91
|
+
""
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
def format_unexpected_field(code:, value:)
|
96
|
+
sprintf("%s%s|", code, value)
|
97
|
+
end
|
98
|
+
|
99
|
+
def format_unexpected_fields
|
100
|
+
|
101
|
+
if self.attributes.key?(:unexpected_fields)
|
102
|
+
Array(self[:unexpected_fields])
|
103
|
+
.map { |f| format_unexpected_field(**f) }
|
104
|
+
.join
|
105
|
+
else
|
106
|
+
""
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def encode(add_error_detection: true, checksum_encoder: nil, strip_extra_fields: false)
|
111
|
+
message = sprintf(
|
112
|
+
"%<code>s%<ordered_fields>s%<delimited_fields>s%<unexpected_fields>s",
|
113
|
+
code: self.class::CODE,
|
114
|
+
ordered_fields:
|
115
|
+
ordered_fields.map { |f| format_field(f) }.join,
|
116
|
+
delimited_fields:
|
117
|
+
delimited_fields.map { |f| format_field(f) }.join,
|
118
|
+
unexpected_fields:
|
119
|
+
if strip_extra_fields then "" else format_unexpected_fields end
|
120
|
+
)
|
121
|
+
error_detection =
|
122
|
+
if add_error_detection
|
123
|
+
error_detection_fields(message, checksum_encoder: checksum_encoder)
|
124
|
+
else
|
125
|
+
""
|
126
|
+
end
|
127
|
+
sprintf("%s%s\r", message, error_detection)
|
128
|
+
end
|
129
|
+
|
130
|
+
def to_s
|
131
|
+
encode
|
132
|
+
end
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require 'dry-struct'
|
2
|
+
require 'sip2/types'
|
3
|
+
module Sip2
|
4
|
+
|
5
|
+
module Message
|
6
|
+
class UnknownMessage < Dry::Struct
|
7
|
+
|
8
|
+
schema schema.strict
|
9
|
+
|
10
|
+
transform_keys(&:to_sym)
|
11
|
+
|
12
|
+
attribute :message_code, Types::String.constrained(format: /^[A-Za-z0-9]{2,2}$/)
|
13
|
+
attribute :message_name, Types::String.default("Unknown Message".freeze)
|
14
|
+
attribute :message_data, Types::String
|
15
|
+
|
16
|
+
def encode
|
17
|
+
to_s
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_s
|
21
|
+
sprintf("%s%s\r", message_code, message_data)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
data/lib/sip2/message.rb
ADDED
@@ -0,0 +1,95 @@
|
|
1
|
+
require 'sip2/fields'
|
2
|
+
require 'sip2/messages'
|
3
|
+
require 'sip2/message/base_message'
|
4
|
+
require 'sip2/message/unknown_message'
|
5
|
+
|
6
|
+
module Sip2
|
7
|
+
module Message
|
8
|
+
|
9
|
+
BY_CODE = {}
|
10
|
+
BY_SYMBOL = {}
|
11
|
+
|
12
|
+
def self.[](code)
|
13
|
+
BY_CODE[code]
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.from_hash(data)
|
17
|
+
code = data.fetch(:message_code) { data.fetch("message_code") }
|
18
|
+
klass = self[code] || UnknownMessage
|
19
|
+
klass.new(data)
|
20
|
+
end
|
21
|
+
|
22
|
+
MESSAGES.each do |msg|
|
23
|
+
|
24
|
+
code = msg.fetch(:code)
|
25
|
+
name = msg.fetch(:name)
|
26
|
+
symbol = msg.fetch(:symbol)
|
27
|
+
|
28
|
+
class_name = symbol.to_s.split("_").map(&:capitalize).join
|
29
|
+
|
30
|
+
ordered_fields = msg.fetch(:ordered_fields)
|
31
|
+
required_delimited_fields = msg.fetch(:required_delimited_fields)
|
32
|
+
optional_delimited_fields = msg.fetch(:optional_delimited_fields)
|
33
|
+
|
34
|
+
struct = Class.new(Sip2::Message::BaseMessage) do
|
35
|
+
self::CODE = code
|
36
|
+
self::NAME = name
|
37
|
+
|
38
|
+
attribute :message_code, Types::String.constrained(eql: code).default(code)
|
39
|
+
attribute :message_name, Types::String.constrained(eql: name).default(name)
|
40
|
+
|
41
|
+
@ordered_fields = ordered_fields
|
42
|
+
@required_delimited_fields = required_delimited_fields
|
43
|
+
@optional_delimited_fields = optional_delimited_fields
|
44
|
+
|
45
|
+
(ordered_fields + required_delimited_fields).each do |field_name|
|
46
|
+
field_info = Sip2::FIELDS.fetch(field_name)
|
47
|
+
attribute_name = field_info.fetch(:name, field_name)
|
48
|
+
field_type = field_info.fetch(:type)
|
49
|
+
if field_type.is_a?(Hash)
|
50
|
+
attribute attribute_name do
|
51
|
+
field_type.each do |subfield_name,subfield_type|
|
52
|
+
attribute subfield_name, subfield_type
|
53
|
+
end
|
54
|
+
end
|
55
|
+
else
|
56
|
+
attribute attribute_name, field_type
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
optional_delimited_fields.each do |field_name|
|
61
|
+
field_info = Sip2::FIELDS.fetch(field_name)
|
62
|
+
field_type = field_info.fetch(:type)
|
63
|
+
if field_type.is_a?(Hash)
|
64
|
+
attribute? field_name do
|
65
|
+
field_type.each do |subfield_name,subfield_type|
|
66
|
+
attribute subfield_name, subfield_type
|
67
|
+
end
|
68
|
+
end
|
69
|
+
else
|
70
|
+
attribute? field_name, field_type
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
attribute? :unexpected_fields, Types::Array do
|
75
|
+
attribute :code, Types::String
|
76
|
+
attribute :value, Types::String
|
77
|
+
end
|
78
|
+
|
79
|
+
unless [:request_asc_resend, :request_sc_resend].include?(symbol)
|
80
|
+
attribute? :sequence_number, Types::Integer.constrained(included_in: 0..9)
|
81
|
+
end
|
82
|
+
attribute? :checksum, Types::String.constrained(format: /^[0-9A-Fa-f]{4,4}$/)
|
83
|
+
|
84
|
+
end
|
85
|
+
|
86
|
+
BY_CODE[code] = struct
|
87
|
+
BY_SYMBOL[symbol] = struct
|
88
|
+
Message.const_set(class_name, struct)
|
89
|
+
|
90
|
+
end
|
91
|
+
|
92
|
+
BY_CODE.freeze
|
93
|
+
BY_SYMBOL.freeze
|
94
|
+
end
|
95
|
+
end
|
@@ -0,0 +1,122 @@
|
|
1
|
+
require 'parslet'
|
2
|
+
require 'sip2/field_parser_rules'
|
3
|
+
require 'sip2/messages'
|
4
|
+
|
5
|
+
module Sip2
|
6
|
+
module MessageParserRules
|
7
|
+
include Parslet
|
8
|
+
|
9
|
+
include Sip2::FieldParserRules
|
10
|
+
|
11
|
+
rule(:newline) { str("\n") }
|
12
|
+
|
13
|
+
rule(:empty_hash) {
|
14
|
+
str("").as(:empty_hash)
|
15
|
+
}
|
16
|
+
|
17
|
+
rule(:no_ordered_fields) {
|
18
|
+
empty_hash.as(:ordered_fields)
|
19
|
+
}
|
20
|
+
|
21
|
+
rule(:no_delimited_fields) {
|
22
|
+
empty_hash.as(:delimited_fields)
|
23
|
+
}
|
24
|
+
|
25
|
+
rule(:error_detection) {
|
26
|
+
( (sequence_number >> checksum) | empty_hash ).as(:error_detection)
|
27
|
+
}
|
28
|
+
|
29
|
+
rule(:checksum_only) {
|
30
|
+
( checksum | empty_hash ).as(:error_detection)
|
31
|
+
}
|
32
|
+
|
33
|
+
rule(:unexpected_field) {
|
34
|
+
(
|
35
|
+
any_field_identifier.as(:code) >>
|
36
|
+
any_valid.repeat.as(:str).as(:value) >>
|
37
|
+
pipe
|
38
|
+
)
|
39
|
+
}
|
40
|
+
|
41
|
+
rule(:unexpected_fields) {
|
42
|
+
unexpected_field.as(:unexpected_fields).as(:merge_repeat_to_array)
|
43
|
+
}
|
44
|
+
|
45
|
+
rule(:known_message_id) {
|
46
|
+
Sip2::MESSAGES_BY_CODE.keys.map { |s| str(s) }.inject { |res,a| res | a }
|
47
|
+
}
|
48
|
+
|
49
|
+
# A *command identifier* is defined as "two ASCII-characters". Theoretically
|
50
|
+
# that means any ASCII-character except for the NULL-character (which is
|
51
|
+
# disallowed) and carriage return (which might only be used as message
|
52
|
+
# terminator).
|
53
|
+
#
|
54
|
+
# However, all existing messages have two digit identifiers. It is highly
|
55
|
+
# unlikely that punctuation will be used as command identifiers. For
|
56
|
+
# simplicity we reduce the accepted character range to digits and letters.
|
57
|
+
#
|
58
|
+
rule(:unknown_message_id) {
|
59
|
+
known_message_id.absent? >> match["0-9A-Za-z"].repeat(2,2).as(:str)
|
60
|
+
}
|
61
|
+
|
62
|
+
MESSAGES.each do |msg|
|
63
|
+
ordered = msg.fetch(:ordered_fields)
|
64
|
+
required_delimited = msg.fetch(:required_delimited_fields)
|
65
|
+
optional_delimited = msg.fetch(:optional_delimited_fields)
|
66
|
+
unexpected_delimited = [ :unexpected_fields ]
|
67
|
+
delimited = required_delimited + optional_delimited + unexpected_delimited
|
68
|
+
|
69
|
+
rule(msg[:symbol]) {
|
70
|
+
str(msg[:code]).as(:str).as(:message_code).as(:message_identifiers) >>
|
71
|
+
(
|
72
|
+
if ordered.any?
|
73
|
+
ordered
|
74
|
+
.map { |f| self.send(f) }
|
75
|
+
.reduce { |res,atom| res >> atom }
|
76
|
+
else
|
77
|
+
empty_hash
|
78
|
+
end
|
79
|
+
).as(:ordered_fields) >>
|
80
|
+
(
|
81
|
+
if delimited.any?
|
82
|
+
delimited
|
83
|
+
.map { |f| self.send(f) }
|
84
|
+
.reduce { |res,atom| res | atom }
|
85
|
+
.repeat.as(:merge_repeat)
|
86
|
+
else
|
87
|
+
empty_hash
|
88
|
+
end
|
89
|
+
).as(:delimited_fields) >>
|
90
|
+
(
|
91
|
+
if [:request_sc_resend, :request_asc_resend].include?(msg[:symbol])
|
92
|
+
checksum_only
|
93
|
+
else
|
94
|
+
error_detection
|
95
|
+
end
|
96
|
+
) >>
|
97
|
+
eom
|
98
|
+
}
|
99
|
+
end
|
100
|
+
|
101
|
+
rule(:unknown_message_data) {
|
102
|
+
(eom.absent? >> any).repeat.as(:str) >> eom
|
103
|
+
}
|
104
|
+
|
105
|
+
# Messages with unknown ID:s should be accepted and ignored
|
106
|
+
rule(:unknown_message) {
|
107
|
+
unknown_message_id.as(:message_code) >>
|
108
|
+
unknown_message_data.as(:message_data)
|
109
|
+
}
|
110
|
+
|
111
|
+
rule(:known_message) {
|
112
|
+
MESSAGES
|
113
|
+
.map { |msg| self.send(msg[:symbol]) }
|
114
|
+
.reduce { |res,atom| res | atom }
|
115
|
+
}
|
116
|
+
|
117
|
+
rule(:messages) {
|
118
|
+
(( known_message | unknown_message) >> newline.maybe).repeat
|
119
|
+
}
|
120
|
+
|
121
|
+
end
|
122
|
+
end
|