sashite-qpi 2.0.0 → 2.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 +4 -4
- data/LICENSE +201 -0
- data/README.md +208 -491
- data/lib/sashite/qpi/constants.rb +13 -0
- data/lib/sashite/qpi/errors/argument/messages.rb +23 -0
- data/lib/sashite/qpi/errors/argument.rb +16 -0
- data/lib/sashite/qpi/errors.rb +3 -0
- data/lib/sashite/qpi/identifier.rb +156 -153
- data/lib/sashite/qpi/parser.rb +122 -0
- data/lib/sashite/qpi.rb +57 -100
- data/lib/sashite-qpi.rb +0 -11
- metadata +18 -17
- data/LICENSE.md +0 -22
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Qpi
|
|
5
|
+
# Constants for QPI (Qualified Piece Identifier).
|
|
6
|
+
#
|
|
7
|
+
# This module defines the structural constants for QPI tokens.
|
|
8
|
+
module Constants
|
|
9
|
+
# Separator between SIN and PIN components.
|
|
10
|
+
SEPARATOR = ":"
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Sashite
|
|
4
|
+
module Qpi
|
|
5
|
+
module Errors
|
|
6
|
+
class Argument < ::ArgumentError
|
|
7
|
+
# Centralized error messages for QPI parsing and validation.
|
|
8
|
+
#
|
|
9
|
+
# @example
|
|
10
|
+
# raise Errors::Argument, Messages::EMPTY_INPUT
|
|
11
|
+
module Messages
|
|
12
|
+
# Parsing errors
|
|
13
|
+
EMPTY_INPUT = "empty input"
|
|
14
|
+
MISSING_SEPARATOR = "missing colon separator"
|
|
15
|
+
MISSING_SIN = "missing SIN component"
|
|
16
|
+
MISSING_PIN = "missing PIN component"
|
|
17
|
+
INVALID_SIN = "invalid SIN component"
|
|
18
|
+
INVALID_PIN = "invalid PIN component"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "argument/messages"
|
|
4
|
+
|
|
5
|
+
module Sashite
|
|
6
|
+
module Qpi
|
|
7
|
+
module Errors
|
|
8
|
+
# Error raised when QPI parsing or validation fails.
|
|
9
|
+
#
|
|
10
|
+
# @example
|
|
11
|
+
# raise Argument, Argument::Messages::EMPTY_INPUT
|
|
12
|
+
class Argument < ::ArgumentError
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,95 +1,62 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
require_relative "errors"
|
|
5
5
|
|
|
6
6
|
module Sashite
|
|
7
7
|
module Qpi
|
|
8
|
-
# Represents
|
|
8
|
+
# Represents a parsed QPI (Qualified Piece Identifier) identifier.
|
|
9
9
|
#
|
|
10
|
-
# QPI
|
|
11
|
-
#
|
|
10
|
+
# A QPI identifier encodes complete Piece Identity by combining:
|
|
11
|
+
# - A SIN (Style Identifier Notation) component for Piece Style
|
|
12
|
+
# - A PIN (Piece Identifier Notation) component for Piece Name, Side, State, and Terminal Status
|
|
12
13
|
#
|
|
13
|
-
#
|
|
14
|
+
# Additionally, QPI defines a Native/Derived relationship based on case comparison
|
|
15
|
+
# between the SIN and PIN letters.
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
# 1. new(sin, pin) — create from components with validation
|
|
17
|
-
# 2. sin — access SIN component
|
|
18
|
-
# 3. pin — access PIN component
|
|
19
|
-
# 4. to_s — serialize to QPI string
|
|
20
|
-
# 5. flip — flip both components (only convenience method)
|
|
17
|
+
# Instances are immutable (frozen after creation).
|
|
21
18
|
#
|
|
22
|
-
#
|
|
23
|
-
# - with_sin(new_sin) — create identifier with different SIN
|
|
24
|
-
# - with_pin(new_pin) — create identifier with different PIN
|
|
25
|
-
#
|
|
26
|
-
# All other operations use the component APIs directly:
|
|
27
|
-
# - qpi.sin.family — access Piece Style
|
|
28
|
-
# - qpi.sin.side — access Piece Side
|
|
29
|
-
# - qpi.pin.type — access Piece Name
|
|
30
|
-
# - qpi.pin.state — access Piece State
|
|
31
|
-
# - qpi.pin.terminal? — access Terminal Status
|
|
32
|
-
#
|
|
33
|
-
# ## Why Only flip as Convenience?
|
|
34
|
-
#
|
|
35
|
-
# flip is the ONLY transformation that naturally operates on both
|
|
36
|
-
# SIN and PIN components simultaneously. All other transformations
|
|
37
|
-
# work through component replacement:
|
|
38
|
-
#
|
|
39
|
-
# qpi.with_sin(qpi.sin.with_family(:S)) # Transform SIN
|
|
40
|
-
# qpi.with_pin(qpi.pin.with_type(:Q)) # Transform PIN
|
|
41
|
-
# qpi.with_pin(qpi.pin.with_terminal(true)) # Transform PIN
|
|
42
|
-
#
|
|
43
|
-
# This avoids arbitrary conveniences and maintains a clear principle.
|
|
44
|
-
#
|
|
45
|
-
# @example Pure composition
|
|
19
|
+
# @example Creating identifiers
|
|
46
20
|
# sin = Sashite::Sin.parse("C")
|
|
47
21
|
# pin = Sashite::Pin.parse("K^")
|
|
48
|
-
# qpi =
|
|
49
|
-
# qpi.to_s # => "C:K^"
|
|
50
|
-
# qpi.sin # => SIN::Identifier
|
|
51
|
-
# qpi.pin # => PIN::Identifier
|
|
22
|
+
# qpi = Identifier.new(sin, pin)
|
|
52
23
|
#
|
|
53
|
-
# @example
|
|
54
|
-
# qpi.sin.
|
|
55
|
-
# qpi.pin.type
|
|
56
|
-
# qpi.
|
|
57
|
-
# qpi.pin.state
|
|
58
|
-
# qpi.pin.terminal?
|
|
24
|
+
# @example Accessing components
|
|
25
|
+
# qpi.sin.style # => :C
|
|
26
|
+
# qpi.pin.type # => :K
|
|
27
|
+
# qpi.pin.side # => :first
|
|
28
|
+
# qpi.pin.state # => :normal
|
|
29
|
+
# qpi.pin.terminal? # => true
|
|
59
30
|
#
|
|
60
|
-
# @example
|
|
61
|
-
# qpi.
|
|
62
|
-
# qpi.
|
|
63
|
-
# qpi.flip # => "c:k^"
|
|
31
|
+
# @example Native/Derived relationship
|
|
32
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("K"))
|
|
33
|
+
# qpi.native? # => true (both uppercase/first)
|
|
64
34
|
#
|
|
65
|
-
#
|
|
35
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("k"))
|
|
36
|
+
# qpi.derived? # => true (SIN uppercase, PIN lowercase)
|
|
37
|
+
#
|
|
38
|
+
# @see https://sashite.dev/specs/qpi/1.0.0/
|
|
66
39
|
class Identifier
|
|
67
|
-
#
|
|
68
|
-
SEPARATOR = ":"
|
|
69
|
-
|
|
70
|
-
# Error messages
|
|
71
|
-
ERROR_INVALID_QPI = "Invalid QPI string: %s"
|
|
72
|
-
ERROR_SEMANTIC_MISMATCH = "SIN and PIN components must have same side: sin.side=%s, pin.side=%s"
|
|
73
|
-
ERROR_MISSING_SEPARATOR = "QPI string must contain exactly one colon separator: %s"
|
|
74
|
-
|
|
75
|
-
# @return [Sin::Identifier] the SIN component
|
|
40
|
+
# @return [Sashite::Sin::Identifier] The SIN component
|
|
76
41
|
attr_reader :sin
|
|
77
42
|
|
|
78
|
-
# @return [Pin::Identifier]
|
|
43
|
+
# @return [Sashite::Pin::Identifier] The PIN component
|
|
79
44
|
attr_reader :pin
|
|
80
45
|
|
|
81
|
-
#
|
|
46
|
+
# Creates a new Identifier instance.
|
|
82
47
|
#
|
|
83
|
-
# @param sin [Sin::Identifier] SIN component
|
|
84
|
-
# @param pin [Pin::Identifier] PIN component
|
|
85
|
-
# @
|
|
48
|
+
# @param sin [Sashite::Sin::Identifier] The SIN component
|
|
49
|
+
# @param pin [Sashite::Pin::Identifier] The PIN component
|
|
50
|
+
# @return [Identifier] A new frozen Identifier instance
|
|
51
|
+
# @raise [Errors::Argument] If components are invalid
|
|
86
52
|
#
|
|
87
53
|
# @example
|
|
88
54
|
# sin = Sashite::Sin.parse("C")
|
|
89
55
|
# pin = Sashite::Pin.parse("K^")
|
|
90
|
-
#
|
|
56
|
+
# Identifier.new(sin, pin)
|
|
91
57
|
def initialize(sin, pin)
|
|
92
|
-
|
|
58
|
+
validate_sin!(sin)
|
|
59
|
+
validate_pin!(pin)
|
|
93
60
|
|
|
94
61
|
@sin = sin
|
|
95
62
|
@pin = pin
|
|
@@ -97,144 +64,180 @@ module Sashite
|
|
|
97
64
|
freeze
|
|
98
65
|
end
|
|
99
66
|
|
|
100
|
-
#
|
|
67
|
+
# ========================================================================
|
|
68
|
+
# String Conversion
|
|
69
|
+
# ========================================================================
|
|
70
|
+
|
|
71
|
+
# Returns the QPI string representation.
|
|
101
72
|
#
|
|
102
|
-
# @
|
|
103
|
-
# @return [Identifier] new identifier instance
|
|
104
|
-
# @raise [ArgumentError] if invalid or semantically inconsistent
|
|
73
|
+
# @return [String] The QPI string in format "SIN:PIN"
|
|
105
74
|
#
|
|
106
75
|
# @example
|
|
107
|
-
# qpi
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
string_value = String(qpi_string)
|
|
112
|
-
sin_part, pin_part = split_components(string_value)
|
|
76
|
+
# qpi.to_s # => "C:K^"
|
|
77
|
+
def to_s
|
|
78
|
+
"#{sin}#{Constants::SEPARATOR}#{pin}"
|
|
79
|
+
end
|
|
113
80
|
|
|
114
|
-
|
|
115
|
-
|
|
81
|
+
# ========================================================================
|
|
82
|
+
# Native/Derived Relationship
|
|
83
|
+
# ========================================================================
|
|
116
84
|
|
|
117
|
-
|
|
85
|
+
# Checks if the identifier is native.
|
|
86
|
+
#
|
|
87
|
+
# A QPI is native when sin.side equals pin.side (same case).
|
|
88
|
+
#
|
|
89
|
+
# @return [Boolean] true if native
|
|
90
|
+
#
|
|
91
|
+
# @example
|
|
92
|
+
# Identifier.new(Sin.parse("C"), Pin.parse("K")).native? # => true
|
|
93
|
+
# Identifier.new(Sin.parse("c"), Pin.parse("k")).native? # => true
|
|
94
|
+
# Identifier.new(Sin.parse("C"), Pin.parse("k")).native? # => false
|
|
95
|
+
def native?
|
|
96
|
+
sin.side.equal?(pin.side)
|
|
118
97
|
end
|
|
119
98
|
|
|
120
|
-
#
|
|
99
|
+
# Checks if the identifier is derived.
|
|
121
100
|
#
|
|
122
|
-
#
|
|
123
|
-
#
|
|
101
|
+
# A QPI is derived when sin.side differs from pin.side (different case).
|
|
102
|
+
#
|
|
103
|
+
# @return [Boolean] true if derived
|
|
124
104
|
#
|
|
125
105
|
# @example
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
sin_part, pin_part = split_components(qpi_string)
|
|
132
|
-
return false unless Sashite::Sin.valid?(sin_part) && Sashite::Pin.valid?(pin_part)
|
|
133
|
-
|
|
134
|
-
sin_identifier = Sashite::Sin.parse(sin_part)
|
|
135
|
-
pin_identifier = Sashite::Pin.parse(pin_part)
|
|
136
|
-
sin_identifier.side == pin_identifier.side
|
|
137
|
-
rescue ArgumentError
|
|
138
|
-
false
|
|
106
|
+
# Identifier.new(Sin.parse("C"), Pin.parse("k")).derived? # => true
|
|
107
|
+
# Identifier.new(Sin.parse("c"), Pin.parse("K")).derived? # => true
|
|
108
|
+
# Identifier.new(Sin.parse("C"), Pin.parse("K")).derived? # => false
|
|
109
|
+
def derived?
|
|
110
|
+
!native?
|
|
139
111
|
end
|
|
140
112
|
|
|
141
|
-
#
|
|
113
|
+
# ========================================================================
|
|
114
|
+
# Native/Derived Transformations
|
|
115
|
+
# ========================================================================
|
|
116
|
+
|
|
117
|
+
# Returns a new Identifier with PIN case aligned to SIN case.
|
|
118
|
+
#
|
|
119
|
+
# If already native, returns self.
|
|
142
120
|
#
|
|
143
|
-
# @return [
|
|
121
|
+
# @return [Identifier] A native Identifier
|
|
144
122
|
#
|
|
145
123
|
# @example
|
|
146
|
-
# qpi
|
|
147
|
-
|
|
148
|
-
|
|
124
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("r"))
|
|
125
|
+
# qpi.native.to_s # => "C:R"
|
|
126
|
+
#
|
|
127
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("R"))
|
|
128
|
+
# qpi.native.to_s # => "C:R" (unchanged)
|
|
129
|
+
def native
|
|
130
|
+
return self if native?
|
|
131
|
+
|
|
132
|
+
self.class.new(sin, pin.with_side(sin.side))
|
|
149
133
|
end
|
|
150
134
|
|
|
151
|
-
#
|
|
135
|
+
# Returns a new Identifier with PIN case opposite to SIN case.
|
|
136
|
+
#
|
|
137
|
+
# If already derived, returns self.
|
|
152
138
|
#
|
|
153
|
-
# @
|
|
154
|
-
# @return [Identifier] new identifier instance
|
|
155
|
-
# @raise [ArgumentError] if new SIN has different side than PIN
|
|
139
|
+
# @return [Identifier] A derived Identifier
|
|
156
140
|
#
|
|
157
141
|
# @example
|
|
158
|
-
# qpi.
|
|
159
|
-
|
|
160
|
-
|
|
142
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("R"))
|
|
143
|
+
# qpi.derive.to_s # => "C:r"
|
|
144
|
+
#
|
|
145
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("r"))
|
|
146
|
+
# qpi.derive.to_s # => "C:r" (unchanged)
|
|
147
|
+
def derive
|
|
148
|
+
return self if derived?
|
|
161
149
|
|
|
162
|
-
|
|
150
|
+
opposite_side = sin.side.equal?(:first) ? :second : :first
|
|
151
|
+
self.class.new(sin, pin.with_side(opposite_side))
|
|
163
152
|
end
|
|
164
153
|
|
|
165
|
-
#
|
|
154
|
+
# ========================================================================
|
|
155
|
+
# Component Transformations
|
|
156
|
+
# ========================================================================
|
|
157
|
+
|
|
158
|
+
# Returns a new Identifier with a different SIN component.
|
|
166
159
|
#
|
|
167
|
-
# @param
|
|
168
|
-
# @return [Identifier] new
|
|
169
|
-
# @raise [
|
|
160
|
+
# @param new_sin [Sashite::Sin::Identifier] The new SIN component
|
|
161
|
+
# @return [Identifier] A new Identifier with the specified SIN
|
|
162
|
+
# @raise [Errors::Argument] If the SIN is invalid
|
|
170
163
|
#
|
|
171
164
|
# @example
|
|
172
|
-
# qpi.
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
self.class.new(@sin, new_pin)
|
|
165
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("K"))
|
|
166
|
+
# qpi.with_sin(Sin.parse("S")).to_s # => "S:K"
|
|
167
|
+
def with_sin(new_sin)
|
|
168
|
+
self.class.new(new_sin, pin)
|
|
177
169
|
end
|
|
178
170
|
|
|
179
|
-
#
|
|
171
|
+
# Returns a new Identifier with a different PIN component.
|
|
180
172
|
#
|
|
181
|
-
#
|
|
182
|
-
#
|
|
173
|
+
# @param new_pin [Sashite::Pin::Identifier] The new PIN component
|
|
174
|
+
# @return [Identifier] A new Identifier with the specified PIN
|
|
175
|
+
# @raise [Errors::Argument] If the PIN is invalid
|
|
176
|
+
#
|
|
177
|
+
# @example
|
|
178
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("K"))
|
|
179
|
+
# qpi.with_pin(Pin.parse("+Q^")).to_s # => "C:+Q^"
|
|
180
|
+
def with_pin(new_pin)
|
|
181
|
+
self.class.new(sin, new_pin)
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
# Returns a new Identifier with both components flipped.
|
|
183
185
|
#
|
|
184
|
-
# @return [Identifier] new
|
|
186
|
+
# @return [Identifier] A new Identifier with both SIN and PIN flipped
|
|
185
187
|
#
|
|
186
188
|
# @example
|
|
187
|
-
# qpi =
|
|
188
|
-
# qpi.flip # => "c:k^"
|
|
189
|
+
# qpi = Identifier.new(Sin.parse("C"), Pin.parse("K^"))
|
|
190
|
+
# qpi.flip.to_s # => "c:k^"
|
|
189
191
|
def flip
|
|
190
|
-
self.class.new(
|
|
192
|
+
self.class.new(sin.flip, pin.flip)
|
|
191
193
|
end
|
|
192
194
|
|
|
193
|
-
#
|
|
195
|
+
# ========================================================================
|
|
196
|
+
# Equality
|
|
197
|
+
# ========================================================================
|
|
198
|
+
|
|
199
|
+
# Checks equality with another Identifier.
|
|
194
200
|
#
|
|
195
|
-
# @param other [Object] object to compare
|
|
196
|
-
# @return [Boolean] true if
|
|
201
|
+
# @param other [Object] The object to compare
|
|
202
|
+
# @return [Boolean] true if equal
|
|
197
203
|
def ==(other)
|
|
198
|
-
return false unless
|
|
204
|
+
return false unless self.class === other
|
|
199
205
|
|
|
200
|
-
|
|
206
|
+
sin == other.sin && pin == other.pin
|
|
201
207
|
end
|
|
202
208
|
|
|
203
|
-
# Alias for == to ensure Set functionality works correctly
|
|
204
209
|
alias eql? ==
|
|
205
210
|
|
|
206
|
-
#
|
|
211
|
+
# Returns a hash code for the Identifier.
|
|
207
212
|
#
|
|
208
|
-
# @return [Integer]
|
|
213
|
+
# @return [Integer] Hash code
|
|
209
214
|
def hash
|
|
210
|
-
[
|
|
215
|
+
[sin, pin].hash
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
# Returns an inspect string for the Identifier.
|
|
219
|
+
#
|
|
220
|
+
# @return [String] Inspect representation
|
|
221
|
+
def inspect
|
|
222
|
+
"#<#{self.class} #{self}>"
|
|
211
223
|
end
|
|
212
224
|
|
|
213
225
|
private
|
|
214
226
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
# @return [Array<String>] array containing [sin_part, pin_part]
|
|
219
|
-
# @raise [ArgumentError] if string doesn't contain exactly one separator
|
|
220
|
-
def self.split_components(qpi_string)
|
|
221
|
-
parts = qpi_string.split(SEPARATOR, 2)
|
|
222
|
-
raise ::ArgumentError, format(ERROR_MISSING_SEPARATOR, qpi_string) unless parts.size == 2
|
|
227
|
+
# ========================================================================
|
|
228
|
+
# Private Validation
|
|
229
|
+
# ========================================================================
|
|
223
230
|
|
|
224
|
-
|
|
225
|
-
|
|
231
|
+
def validate_sin!(sin)
|
|
232
|
+
return if sin.is_a?(Sashite::Sin::Identifier)
|
|
226
233
|
|
|
227
|
-
|
|
234
|
+
raise Errors::Argument, Errors::Argument::Messages::INVALID_SIN
|
|
235
|
+
end
|
|
228
236
|
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
# @param sin [Sin::Identifier] SIN component to validate
|
|
232
|
-
# @param pin [Pin::Identifier] PIN component to validate
|
|
233
|
-
# @raise [ArgumentError] if sides don't match
|
|
234
|
-
def validate_semantic_consistency(sin, pin)
|
|
235
|
-
return if sin.side == pin.side
|
|
237
|
+
def validate_pin!(pin)
|
|
238
|
+
return if pin.is_a?(Sashite::Pin::Identifier)
|
|
236
239
|
|
|
237
|
-
raise ::
|
|
240
|
+
raise Errors::Argument, Errors::Argument::Messages::INVALID_PIN
|
|
238
241
|
end
|
|
239
242
|
end
|
|
240
243
|
end
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sashite/sin"
|
|
4
|
+
require "sashite/pin"
|
|
5
|
+
|
|
6
|
+
require_relative "constants"
|
|
7
|
+
require_relative "errors"
|
|
8
|
+
|
|
9
|
+
module Sashite
|
|
10
|
+
module Qpi
|
|
11
|
+
# Parser for QPI (Qualified Piece Identifier) strings.
|
|
12
|
+
#
|
|
13
|
+
# This parser splits the QPI string on the colon separator and delegates
|
|
14
|
+
# parsing of each component to the SIN and PIN libraries.
|
|
15
|
+
#
|
|
16
|
+
# @example
|
|
17
|
+
# Parser.parse("C:K") # => { sin: <Sin::Identifier>, pin: <Pin::Identifier> }
|
|
18
|
+
# Parser.parse("s:+r^") # => { sin: <Sin::Identifier>, pin: <Pin::Identifier> }
|
|
19
|
+
#
|
|
20
|
+
# @see https://sashite.dev/specs/qpi/1.0.0/
|
|
21
|
+
module Parser
|
|
22
|
+
# Parses a QPI string into its components.
|
|
23
|
+
#
|
|
24
|
+
# @param input [String] The QPI string to parse
|
|
25
|
+
# @return [Hash] A hash with :sin and :pin keys containing Identifier instances
|
|
26
|
+
# @raise [Errors::Argument] If the input is not a valid QPI string
|
|
27
|
+
def self.parse(input)
|
|
28
|
+
validate_input_type(input)
|
|
29
|
+
validate_not_empty(input)
|
|
30
|
+
|
|
31
|
+
sin_string, pin_string = split_components(input)
|
|
32
|
+
|
|
33
|
+
sin = parse_sin(sin_string)
|
|
34
|
+
pin = parse_pin(pin_string)
|
|
35
|
+
|
|
36
|
+
{ sin: sin, pin: pin }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Validates a QPI string without raising an exception.
|
|
40
|
+
#
|
|
41
|
+
# @param input [String] The QPI string to validate
|
|
42
|
+
# @return [Boolean] true if valid, false otherwise
|
|
43
|
+
def self.valid?(input)
|
|
44
|
+
return false unless ::String === input
|
|
45
|
+
|
|
46
|
+
parse(input)
|
|
47
|
+
true
|
|
48
|
+
rescue Errors::Argument
|
|
49
|
+
false
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
class << self
|
|
53
|
+
private
|
|
54
|
+
|
|
55
|
+
# Validates that input is a String.
|
|
56
|
+
#
|
|
57
|
+
# @param input [Object] The input to validate
|
|
58
|
+
# @raise [Errors::Argument] If input is not a String
|
|
59
|
+
def validate_input_type(input)
|
|
60
|
+
return if ::String === input
|
|
61
|
+
|
|
62
|
+
raise Errors::Argument, Errors::Argument::Messages::EMPTY_INPUT
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Validates that input is not empty.
|
|
66
|
+
#
|
|
67
|
+
# @param input [String] The input to validate
|
|
68
|
+
# @raise [Errors::Argument] If input is empty
|
|
69
|
+
def validate_not_empty(input)
|
|
70
|
+
return unless input.empty?
|
|
71
|
+
|
|
72
|
+
raise Errors::Argument, Errors::Argument::Messages::EMPTY_INPUT
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Splits the input into SIN and PIN components.
|
|
76
|
+
#
|
|
77
|
+
# @param input [String] The QPI string to split
|
|
78
|
+
# @return [Array<String>] An array with [sin_string, pin_string]
|
|
79
|
+
# @raise [Errors::Argument] If the separator is missing or components are empty
|
|
80
|
+
def split_components(input)
|
|
81
|
+
unless input.include?(Constants::SEPARATOR)
|
|
82
|
+
raise Errors::Argument, Errors::Argument::Messages::MISSING_SEPARATOR
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
sin_string, pin_string = input.split(Constants::SEPARATOR, 2)
|
|
86
|
+
|
|
87
|
+
if sin_string.nil? || sin_string.empty?
|
|
88
|
+
raise Errors::Argument, Errors::Argument::Messages::MISSING_SIN
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if pin_string.nil? || pin_string.empty?
|
|
92
|
+
raise Errors::Argument, Errors::Argument::Messages::MISSING_PIN
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
[sin_string, pin_string]
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# Parses the SIN component.
|
|
99
|
+
#
|
|
100
|
+
# @param sin_string [String] The SIN string to parse
|
|
101
|
+
# @return [Sashite::Sin::Identifier] The parsed SIN identifier
|
|
102
|
+
# @raise [Errors::Argument] If the SIN is invalid
|
|
103
|
+
def parse_sin(sin_string)
|
|
104
|
+
Sashite::Sin.parse(sin_string)
|
|
105
|
+
rescue ::ArgumentError => e
|
|
106
|
+
raise Errors::Argument, "#{Errors::Argument::Messages::INVALID_SIN}: #{e.message}"
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Parses the PIN component.
|
|
110
|
+
#
|
|
111
|
+
# @param pin_string [String] The PIN string to parse
|
|
112
|
+
# @return [Sashite::Pin::Identifier] The parsed PIN identifier
|
|
113
|
+
# @raise [Errors::Argument] If the PIN is invalid
|
|
114
|
+
def parse_pin(pin_string)
|
|
115
|
+
Sashite::Pin.parse(pin_string)
|
|
116
|
+
rescue ::ArgumentError => e
|
|
117
|
+
raise Errors::Argument, "#{Errors::Argument::Messages::INVALID_PIN}: #{e.message}"
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
end
|