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,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
@@ -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