From cf49ea94c9e580780508f07c1352ef1252018d0f Mon Sep 17 00:00:00 2001
From: Jeffrey Phillips Freeman <the@jeffreyfreeman.me>
Date: Fri, 4 Aug 2023 23:23:06 -0400
Subject: [PATCH] Updated frame to be a class rather than a map

---
 CHANGELOG.md          |   5 +-
 lib/apex/aprs_kiss.rb |  18 +++---
 lib/apex/frame.rb     | 124 ++++++++++++++++++++++++++++++++++++++++++
 lib/apex/igate_tcp.rb |  22 ++++----
 test/tc_aprs_kiss.rb  |  27 ++++-----
 test/tc_frame.rb      | 122 +++++++++++++++++++++++++++++++++++++++++
 6 files changed, 284 insertions(+), 34 deletions(-)
 create mode 100644 lib/apex/frame.rb
 create mode 100644 test/tc_frame.rb

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 721ec45..f1087bf 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,9 +2,12 @@
 
 ## 1.0.4
 
+* Frames are now classes rather than a map
+* Added mechanism for comparing frames for equivalence.
+
 ## 1.0.3
 
-* Implemented the IGateTcp class for handking internet gateways.
+* Implemented the IGateTcp class for handling internet gateways.
 
 ## 1.0.2
 
diff --git a/lib/apex/aprs_kiss.rb b/lib/apex/aprs_kiss.rb
index 572126d..b0c450e 100644
--- a/lib/apex/aprs_kiss.rb
+++ b/lib/apex/aprs_kiss.rb
@@ -1,4 +1,5 @@
 require 'kiss/constants'
+require 'apex/frame'
 
 module Apex
     class AprsKiss
@@ -11,7 +12,6 @@ module Apex
 
         private
         def self.decode_frame(raw_frame)
-            frame = {}
             frame_len = raw_frame.length
 
             if frame_len > 16
@@ -23,11 +23,11 @@ module Apex
                         if 1.0 < i and i < 11.0
                             if raw_frame[raw_slice + 1] & 0x03 == 0x03 and [0xf0, 0xcf].include? raw_frame[raw_slice + 2]
                                 payload_as_array = raw_frame[raw_slice + 3..-1].map { |b| b.chr }
-                                frame[:payload] = payload_as_array.join
-                                frame[:destination] = identity_as_string(extract_callsign(raw_frame))
-                                frame[:source] = identity_as_string(extract_callsign(raw_frame[7..-1]))
-                                frame[:path] = extract_path(i.to_i, raw_frame)
-                                return frame
+                                payload = payload_as_array.join
+                                destination = identity_as_string(extract_callsign(raw_frame))
+                                source = identity_as_string(extract_callsign(raw_frame[7..-1]))
+                                path = extract_path(i.to_i, raw_frame)
+                                return Frame.new(source, destination, path, payload)
                             end
                         end
                     end
@@ -94,13 +94,13 @@ module Apex
 
         private
         def self.encode_frame(frame)
-            enc_frame = encode_callsign(parse_identity_string(frame[:destination])) + encode_callsign(parse_identity_string(frame[:source]))
+            enc_frame = encode_callsign(parse_identity_string(frame.destination)) + encode_callsign(parse_identity_string(frame.source))
 
-            frame[:path].each do |path|
+            frame.path.each do |path|
                 enc_frame += encode_callsign(parse_identity_string(path))
             end
 
-            return enc_frame[0...-1] + [enc_frame[-1] | 0x01] + [Kiss::SLOT_TIME] + [0xf0] + frame[:payload].chars.map { |c| c.ord }
+            return enc_frame[0...-1] + [enc_frame[-1] | 0x01] + [Kiss::SLOT_TIME] + [0xf0] + frame.payload.chars.map { |c| c.ord }
         end
 
         private
diff --git a/lib/apex/frame.rb b/lib/apex/frame.rb
new file mode 100644
index 0000000..e1a6093
--- /dev/null
+++ b/lib/apex/frame.rb
@@ -0,0 +1,124 @@
+module Apex
+  public
+  class UnpathedFrame
+    attr_accessor :source, :destination, :payload
+
+    protected
+    def initialize(source, destination, payload)
+      @source = source
+      @destination = destination
+      @payload = payload
+    end
+
+    public
+    def path_agnostic_eql?(other)
+      raise ArgumentError.new("The argument must be either an UnpathedFrame or a PathAgnosticFrame") if not ((other.instance_of? UnpathedFrame) || (other.instance_of? PathAgnosticFrame))
+
+      return self == other
+    end
+
+    public
+    def path_agnostic_equality?(other)
+      return false if (not other.respond_to? :source) ||
+                      (not other.respond_to? :destination) ||
+                      (not other.respond_to? :payload)
+
+      if (self.source.eql? other.source) && (self.destination.eql? other.destination) && (self.payload.eql? other.payload)
+         return true
+      else
+        return false
+      end
+    end
+
+    public
+    def path_agnostic_hash
+      return [self.source, self.destination, self.payload].hash
+    end
+
+    public
+    def ==(other)
+      return self.path_agnostic_equality? other
+    end
+
+    public
+    def eql?(other)
+      self.path_agnostic_eql? other
+    end
+
+    public
+    def hash
+      return self.path_agnostic_hash
+    end
+  end
+
+  public
+  class Frame < UnpathedFrame
+    attr_accessor :path
+
+    protected
+    def initialize(source, destination, path, payload)
+      super(source, destination, payload)
+
+      @path = path
+    end
+
+    public
+    def eql?(other)
+      raise ArgumentError.new("The argument can not be a UnpathedFrame or a PathAgnosticFrame") if ((other.instance_of? UnpathedFrame) || (other.instance_of? PathAgnosticFrame))
+      raise ArgumentError.new("The argument must be of type Frame (or a child class).") if not other.kind_of? Frame
+
+      return self == other
+    end
+
+    public
+    def ==(other)
+      return false if not super(other)
+
+      return false if not other.respond_to? :path
+
+      if self.path.eql? other.path
+         return true
+      else
+        return false
+      end
+    end
+
+    public
+    def hash
+      return [super, self.path].hash
+    end
+
+    public
+    def path_agnostic_identity
+      return PathAgnosticFrame.new(self)
+    end
+  end
+
+  private
+  class PathAgnosticFrame < Frame
+    protected
+    def initialize(frame)
+      super(frame.source, frame.destination, frame.path, frame.payload)
+    end
+
+    public
+    def eql?(other)
+      return self.path_agnostic_eql? other
+    end
+
+    public
+    def ==(other)
+      return self.path_agnostic_equality? other
+    end
+
+    public
+    def hash
+      return self.path_agnostic_hash
+    end
+
+    public
+    def path_agnostic_identity
+      return self
+    end
+  end
+end
diff --git a/lib/apex/igate_tcp.rb b/lib/apex/igate_tcp.rb
index 6b5aa2a..09aa6cd 100644
--- a/lib/apex/igate_tcp.rb
+++ b/lib/apex/igate_tcp.rb
@@ -30,23 +30,23 @@ module Apex
 
         private
         def self.encode_frame(frame)
-            formatted_frame = [frame[:source], frame[:destination]].join('>')
-            if frame[:path] and frame[:path].length > 0
-                formatted_frame = [formatted_frame, IGateTcp::format_path(frame[:path])].join(',')
+            formatted_frame = [frame.source, frame.destination].join('>')
+            if frame.path and frame.path.length > 0
+                formatted_frame = [formatted_frame, IGateTcp::format_path(frame.path)].join(',')
             end
             formatted_frame += ':'
-            formatted_frame += frame[:payload]
+            formatted_frame += frame.payload
             return formatted_frame
         end
 
         private
         def self.decode_frame(frame)
-            decoded_frame = {}
+            decoded_source = nil
             frame_so_far = ''
             path = nil
             frame.chars.each do |char|
-                if char == '>' and !decoded_frame.include? :source
-                    decoded_frame[:source] = frame_so_far
+                if char == '>' and decoded_source.nil?
+                    decoded_source = frame_so_far
                     frame_so_far = ''
                 elsif char == ':' and !path
                     path = frame_so_far
@@ -57,11 +57,11 @@ module Apex
             end
 
             path = path.split(',')
-            decoded_frame[:destination] = path.shift
-            decoded_frame[:path] = path
-            decoded_frame[:payload] = frame_so_far
+            decoded_destination = path.shift
+            decoded_path = path
+            decoded_payload = frame_so_far
 
-            decoded_frame
+            return Frame.new(decoded_source, decoded_destination, decoded_path, decoded_payload)
         end
 
         private
diff --git a/test/tc_aprs_kiss.rb b/test/tc_aprs_kiss.rb
index c5058c5..7681c9d 100644
--- a/test/tc_aprs_kiss.rb
+++ b/test/tc_aprs_kiss.rb
@@ -1,30 +1,31 @@
 require 'test/unit'
 require 'kiss/test/kiss_mock'
 require_relative '../lib/apex/aprs_kiss'
+require_relative '../lib/apex/frame'
 
 module Apex
-    DECODED_FRAME_KISS = {
-        :source => 'W2GMD-1',
-        :destination => 'OMG',
-        :path => ['WIDE1-1', 'WIDE2-2'],
-        :payload => 'test_encode_frame'
-    }
+    DECODED_FRAME_KISS = Apex::Frame.new(
+        'W2GMD-1',
+        'OMG',
+        ['WIDE1-1', 'WIDE2-2'],
+        'test_encode_frame'
+    )
     ENCODED_FRAME_KISS = [192, 0, 158, 154, 142, 64, 64, 64, 96, 174, 100, 142, 154, 136, 64, 98, 174, 146, 136, 138, 98,
                           64, 98, 174, 146, 136, 138, 100, 64, 101, 3, 240, 116, 101, 115, 116, 95, 101, 110, 99, 111, 100,
                           101, 95, 102, 114, 97, 109, 101, 192]
 
-    DECODED_FRAME_KISS_INVALID = {
-        :source => 'KG6WTF',
-        :destination => 'S7TSUV',
-        :path => ['MTOSO-2', 'WIDE2*' 'qAR', 'KF6FIR-10'],
-        :payload => '`17El#X-/kg6wtf@gosselinfamily.com'
-    }
+    DECODED_FRAME_KISS_INVALID = Apex::Frame.new(
+        'KG6WTF',
+        'S7TSUV',
+        ['MTOSO-2', 'WIDE2*' 'qAR', 'KF6FIR-10'],
+        '`17El#X-/kg6wtf@gosselinfamily.com'
+    )
     ENCODED_FRAME_KISS_INVALID = [192, 0, 166, 110, 168, 166, 170, 172, 96, 150, 142, 108, 174, 168, 140, 96, 154, 168, 158,
                                   166, 158, 64, 100, 174, 146, 136, 138, 100, 226, 130, 164, 224, 150, 140, 108, 140, 146, 164,
                                   117, 3, 240, 96, 49, 55, 69, 108, 35, 88, 45, 47, 107, 103, 54, 119, 116, 102, 64, 103, 111,
                                   115, 115, 101, 108, 105, 110, 102, 97, 109, 105, 108, 121, 46, 99, 111, 109, 192]
 
-    class TestKiss < Test::Unit::TestCase
+    class TestAprsKiss < Test::Unit::TestCase
         def test_read
             kiss_mock = Kiss::KissMock.new
             aprs_kiss = AprsKiss.new(kiss_mock)
diff --git a/test/tc_frame.rb b/test/tc_frame.rb
new file mode 100644
index 0000000..5463378
--- /dev/null
+++ b/test/tc_frame.rb
@@ -0,0 +1,122 @@
+module Apex
+  BASE_FRAME = Apex::Frame.new(
+      'WI2ARD-1',
+      'OMG',
+      ['WIDE2-2' 'WIDE1-1'],
+      'payload goes here'
+  )
+
+  SAME_BASE_FRAME = Apex::Frame.new(
+      'WI2ARD-1',
+      'OMG',
+      ['WIDE2-2' 'WIDE1-1'],
+      'payload goes here'
+  )
+
+  DIFF_PATH_FRAME = Apex::Frame.new(
+      'WI2ARD-1',
+      'OMG',
+      ['WIDE2-1' 'WIDE1-1'],
+      'payload goes here'
+  )
+
+  DIFF_SOURCE_FRAME = Apex::Frame.new(
+      'WI2ARD-2',
+      'OMG',
+      ['WIDE2-2' 'WIDE1-1'],
+      'payload goes here'
+  )
+
+  DIFF_DEST_FRAME = Apex::Frame.new(
+      'WI2ARD-1',
+      'LOL',
+      ['WIDE2-2' 'WIDE1-1'],
+      'payload goes here'
+  )
+
+  DIFF_PAYLOAD_FRAME = Apex::Frame.new(
+      'WI2ARD-1',
+      'OMG',
+      ['WIDE2-2' 'WIDE1-1'],
+      'totally different payload here'
+  )
+
+  class TestFrame < Test::Unit::TestCase
+    def test_self_frame
+      assert BASE_FRAME.eql? BASE_FRAME
+      assert BASE_FRAME.hash.eql? BASE_FRAME.hash
+    end
+
+    def test_same_frame
+      assert BASE_FRAME.eql? SAME_BASE_FRAME
+      assert BASE_FRAME.hash.eql? SAME_BASE_FRAME.hash
+    end
+
+    def test_same_frame_path_agnostic
+      agnostic_frame = BASE_FRAME.path_agnostic_identity
+      agnostic_other_frame = SAME_BASE_FRAME.path_agnostic_identity
+      assert agnostic_frame.eql? agnostic_other_frame
+      assert agnostic_frame.hash.eql? agnostic_other_frame.hash
+    end
+
+    def test_diff_path_frame
+      assert (not BASE_FRAME.eql? DIFF_PATH_FRAME)
+      assert (not BASE_FRAME.hash.eql? DIFF_PATH_FRAME.hash)
+    end
+
+    def test_diff_path_frame_path_agnostic
+      agnostic_frame = BASE_FRAME.path_agnostic_identity
+      agnostic_other_frame = DIFF_PATH_FRAME.path_agnostic_identity
+      assert agnostic_frame.eql? agnostic_other_frame
+      assert agnostic_frame.hash.eql? agnostic_other_frame.hash
+    end
+
+    def test_diff_source_frame
+      assert (not BASE_FRAME.eql? DIFF_SOURCE_FRAME)
+      assert (not BASE_FRAME.hash.eql? DIFF_SOURCE_FRAME.hash)
+    end
+
+    def test_diff_source_frame_path_agnostic
+      agnostic_frame = BASE_FRAME.path_agnostic_identity
+      agnostic_other_frame = DIFF_SOURCE_FRAME.path_agnostic_identity
+      assert (not agnostic_frame.eql? agnostic_other_frame)
+      assert (not agnostic_frame.hash.eql? agnostic_other_frame.hash)
+    end
+
+    def test_diff_dest_frame
+      assert (not BASE_FRAME.eql? DIFF_DEST_FRAME)
+      assert (not BASE_FRAME.hash.eql? DIFF_DEST_FRAME.hash)
+    end
+
+    def test_diff_dest_frame_path_agnostic
+      agnostic_frame = BASE_FRAME.path_agnostic_identity
+      agnostic_other_frame = DIFF_DEST_FRAME.path_agnostic_identity
+      assert (not agnostic_frame.eql? agnostic_other_frame)
+      assert (not agnostic_frame.hash.eql? agnostic_other_frame.hash)
+    end
+
+    def test_diff_payload_frame
+      assert (not BASE_FRAME.eql? DIFF_PAYLOAD_FRAME)
+      assert (not BASE_FRAME.hash.eql? DIFF_PAYLOAD_FRAME.hash)
+    end
+
+    def test_diff_payload_frame_path_agnostic
+      agnostic_frame = BASE_FRAME.path_agnostic_identity
+      agnostic_other_frame = DIFF_PAYLOAD_FRAME.path_agnostic_identity
+      assert (not agnostic_frame.eql? agnostic_other_frame)
+      assert (not agnostic_frame.hash.eql? agnostic_other_frame.hash)
+    end
+
+    def test_bad_type
+      assert_raise ArgumentError do
+        BASE_FRAME.eql? BASE_FRAME.path_agnostic_identity
+      end
+    end
+
+    def test_bad_type_agnostic
+      assert_raise ArgumentError do
+        BASE_FRAME.path_agnostic_identity.eql? BASE_FRAME
+      end
+    end
+  end
+end
-- 
GitLab