sashite-pin 3.2.0 → 4.0.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 +197 -425
- data/lib/sashite/pin/constants.rb +35 -0
- data/lib/sashite/pin/errors/argument/messages.rb +28 -0
- data/lib/sashite/pin/errors/argument.rb +16 -0
- data/lib/sashite/pin/errors.rb +3 -0
- data/lib/sashite/pin/identifier.rb +294 -271
- data/lib/sashite/pin/parser.rb +170 -0
- data/lib/sashite/pin.rb +72 -46
- metadata +14 -13
- data/LICENSE.md +0 -22
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "constants"
|
|
4
|
+
require_relative "errors"
|
|
5
|
+
|
|
6
|
+
module Sashite
|
|
7
|
+
module Pin
|
|
8
|
+
# Secure parser for PIN (Piece Identifier Notation) strings.
|
|
9
|
+
#
|
|
10
|
+
# This parser uses character-by-character validation without regex
|
|
11
|
+
# to prevent ReDoS attacks and ensure strict ASCII compliance.
|
|
12
|
+
#
|
|
13
|
+
# @example
|
|
14
|
+
# Parser.parse("K") # => { type: :K, side: :first, state: :normal, terminal: false }
|
|
15
|
+
# Parser.parse("+r^") # => { type: :R, side: :second, state: :enhanced, terminal: true }
|
|
16
|
+
#
|
|
17
|
+
# @see https://sashite.dev/specs/pin/1.0.0/
|
|
18
|
+
module Parser
|
|
19
|
+
# Parses a PIN string into its components.
|
|
20
|
+
#
|
|
21
|
+
# @param input [String] The PIN string to parse
|
|
22
|
+
# @return [Hash] A hash with :type, :side, :state, and :terminal keys
|
|
23
|
+
# @raise [Errors::Argument] If the input is not a valid PIN string
|
|
24
|
+
def self.parse(input)
|
|
25
|
+
validate_input_type(input)
|
|
26
|
+
validate_not_empty(input)
|
|
27
|
+
validate_length(input)
|
|
28
|
+
|
|
29
|
+
parse_components(input)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Validates a PIN string without raising an exception.
|
|
33
|
+
#
|
|
34
|
+
# @param input [String] The PIN string to validate
|
|
35
|
+
# @return [Boolean] true if valid, false otherwise
|
|
36
|
+
def self.valid?(input)
|
|
37
|
+
return false unless ::String === input
|
|
38
|
+
|
|
39
|
+
parse(input)
|
|
40
|
+
true
|
|
41
|
+
rescue Errors::Argument
|
|
42
|
+
false
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class << self
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
# Validates that input is a String.
|
|
49
|
+
#
|
|
50
|
+
# @param input [Object] The input to validate
|
|
51
|
+
# @raise [Errors::Argument] If input is not a String
|
|
52
|
+
def validate_input_type(input)
|
|
53
|
+
return if ::String === input
|
|
54
|
+
|
|
55
|
+
raise Errors::Argument, Errors::Argument::Messages::MUST_CONTAIN_ONE_LETTER
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Validates that input is not empty.
|
|
59
|
+
#
|
|
60
|
+
# @param input [String] The input to validate
|
|
61
|
+
# @raise [Errors::Argument] If input is empty
|
|
62
|
+
def validate_not_empty(input)
|
|
63
|
+
return unless input.empty?
|
|
64
|
+
|
|
65
|
+
raise Errors::Argument, Errors::Argument::Messages::EMPTY_INPUT
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Validates that input does not exceed maximum length.
|
|
69
|
+
#
|
|
70
|
+
# @param input [String] The input to validate
|
|
71
|
+
# @raise [Errors::Argument] If input is too long
|
|
72
|
+
def validate_length(input)
|
|
73
|
+
return if input.bytesize <= Constants::MAX_STRING_LENGTH
|
|
74
|
+
|
|
75
|
+
raise Errors::Argument, Errors::Argument::Messages::INPUT_TOO_LONG
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Parses the PIN string into its components.
|
|
79
|
+
#
|
|
80
|
+
# @param input [String] The validated PIN string
|
|
81
|
+
# @return [Hash] A hash with :type, :side, :state, and :terminal keys
|
|
82
|
+
# @raise [Errors::Argument] If the structure is invalid
|
|
83
|
+
def parse_components(input)
|
|
84
|
+
pos = 0
|
|
85
|
+
state = :normal
|
|
86
|
+
terminal = false
|
|
87
|
+
|
|
88
|
+
# Check for state modifier at position 0
|
|
89
|
+
byte = input.getbyte(pos)
|
|
90
|
+
if state_modifier?(byte)
|
|
91
|
+
state = decode_state_modifier(byte)
|
|
92
|
+
pos += 1
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
# Must have a letter at current position
|
|
96
|
+
raise Errors::Argument, Errors::Argument::Messages::MUST_CONTAIN_ONE_LETTER if pos >= input.bytesize
|
|
97
|
+
|
|
98
|
+
byte = input.getbyte(pos)
|
|
99
|
+
raise Errors::Argument, Errors::Argument::Messages::MUST_CONTAIN_ONE_LETTER unless ascii_letter?(byte)
|
|
100
|
+
|
|
101
|
+
type = byte.chr.upcase.to_sym
|
|
102
|
+
side = uppercase_letter?(byte) ? :first : :second
|
|
103
|
+
pos += 1
|
|
104
|
+
|
|
105
|
+
# Check for terminal marker
|
|
106
|
+
if pos < input.bytesize
|
|
107
|
+
byte = input.getbyte(pos)
|
|
108
|
+
raise Errors::Argument, Errors::Argument::Messages::INVALID_TERMINAL_MARKER unless terminal_marker?(byte)
|
|
109
|
+
|
|
110
|
+
terminal = true
|
|
111
|
+
pos += 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Ensure no extra characters
|
|
115
|
+
raise Errors::Argument, Errors::Argument::Messages::MUST_CONTAIN_ONE_LETTER if pos < input.bytesize
|
|
116
|
+
|
|
117
|
+
{ type: type, side: side, state: state, terminal: terminal }
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Checks if a byte is a state modifier (+ or -).
|
|
121
|
+
#
|
|
122
|
+
# @param byte [Integer] The byte to check
|
|
123
|
+
# @return [Boolean] true if state modifier
|
|
124
|
+
def state_modifier?(byte)
|
|
125
|
+
byte == 0x2B || byte == 0x2D # '+' or '-'
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Decodes a state modifier byte to a state symbol.
|
|
129
|
+
#
|
|
130
|
+
# @param byte [Integer] The byte to decode
|
|
131
|
+
# @return [Symbol] :enhanced or :diminished
|
|
132
|
+
def decode_state_modifier(byte)
|
|
133
|
+
byte == 0x2B ? :enhanced : :diminished
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
# Checks if a byte is an ASCII letter (A-Z or a-z).
|
|
137
|
+
#
|
|
138
|
+
# @param byte [Integer] The byte to check
|
|
139
|
+
# @return [Boolean] true if ASCII letter
|
|
140
|
+
def ascii_letter?(byte)
|
|
141
|
+
uppercase_letter?(byte) || lowercase_letter?(byte)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
# Checks if a byte is an uppercase ASCII letter (A-Z).
|
|
145
|
+
#
|
|
146
|
+
# @param byte [Integer] The byte to check
|
|
147
|
+
# @return [Boolean] true if uppercase letter
|
|
148
|
+
def uppercase_letter?(byte)
|
|
149
|
+
byte >= 0x41 && byte <= 0x5A # 'A' to 'Z'
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
# Checks if a byte is a lowercase ASCII letter (a-z).
|
|
153
|
+
#
|
|
154
|
+
# @param byte [Integer] The byte to check
|
|
155
|
+
# @return [Boolean] true if lowercase letter
|
|
156
|
+
def lowercase_letter?(byte)
|
|
157
|
+
byte >= 0x61 && byte <= 0x7A # 'a' to 'z'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# Checks if a byte is a terminal marker (^).
|
|
161
|
+
#
|
|
162
|
+
# @param byte [Integer] The byte to check
|
|
163
|
+
# @return [Boolean] true if terminal marker
|
|
164
|
+
def terminal_marker?(byte)
|
|
165
|
+
byte == 0x5E # '^'
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
end
|
data/lib/sashite/pin.rb
CHANGED
|
@@ -1,68 +1,94 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "pin/constants"
|
|
4
|
+
require_relative "pin/errors"
|
|
3
5
|
require_relative "pin/identifier"
|
|
6
|
+
require_relative "pin/parser"
|
|
4
7
|
|
|
5
8
|
module Sashite
|
|
6
|
-
# PIN (Piece Identifier Notation) implementation for Ruby
|
|
9
|
+
# PIN (Piece Identifier Notation) implementation for Ruby.
|
|
7
10
|
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
11
|
+
# PIN provides an ASCII-based format for representing pieces in abstract strategy
|
|
12
|
+
# board games. It translates piece attributes from the Game Protocol into a compact,
|
|
13
|
+
# portable notation system.
|
|
10
14
|
#
|
|
11
|
-
# Format
|
|
12
|
-
# - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
|
|
13
|
-
# - Letter: A-Z (first player), a-z (second player)
|
|
15
|
+
# == Format
|
|
14
16
|
#
|
|
15
|
-
#
|
|
16
|
-
#
|
|
17
|
-
#
|
|
18
|
-
#
|
|
19
|
-
#
|
|
17
|
+
# [<state-modifier>]<letter>[<terminal-marker>]
|
|
18
|
+
#
|
|
19
|
+
# - *Letter* (+A-Z+, +a-z+): Piece type and side
|
|
20
|
+
# - *State modifier*: <tt>+</tt> (enhanced), <tt>-</tt> (diminished), or none (normal)
|
|
21
|
+
# - *Terminal marker*: <tt>^</tt> (terminal piece) or none
|
|
22
|
+
#
|
|
23
|
+
# == Attributes
|
|
24
|
+
#
|
|
25
|
+
# A PIN token encodes exactly these attributes:
|
|
26
|
+
#
|
|
27
|
+
# - *Piece Name* → one ASCII letter chosen by the Game / Rule System
|
|
28
|
+
# - *Piece Side* → the case of that letter (uppercase = first, lowercase = second)
|
|
29
|
+
# - *Piece State* → an optional prefix (<tt>+</tt> for enhanced, <tt>-</tt> for diminished)
|
|
30
|
+
# - *Terminal status* → an optional suffix (<tt>^</tt>)
|
|
31
|
+
#
|
|
32
|
+
# == Examples
|
|
33
|
+
#
|
|
34
|
+
# pin = Sashite::Pin.parse("K")
|
|
35
|
+
# pin.type # => :K
|
|
36
|
+
# pin.side # => :first
|
|
37
|
+
# pin.state # => :normal
|
|
38
|
+
# pin.terminal? # => false
|
|
39
|
+
#
|
|
40
|
+
# pin = Sashite::Pin.parse("+R")
|
|
41
|
+
# pin.to_s # => "+R"
|
|
42
|
+
#
|
|
43
|
+
# pin = Sashite::Pin.parse("k^")
|
|
44
|
+
# pin.terminal? # => true
|
|
45
|
+
#
|
|
46
|
+
# Sashite::Pin.valid?("K^") # => true
|
|
47
|
+
# Sashite::Pin.valid?("invalid") # => false
|
|
20
48
|
#
|
|
21
49
|
# @see https://sashite.dev/specs/pin/1.0.0/
|
|
22
50
|
module Pin
|
|
23
|
-
#
|
|
51
|
+
# Parses a PIN string into an Identifier.
|
|
24
52
|
#
|
|
25
|
-
# @param
|
|
26
|
-
# @return [
|
|
53
|
+
# @param string [String] The PIN string to parse
|
|
54
|
+
# @return [Identifier] A new Identifier instance
|
|
55
|
+
# @raise [Errors::Argument] If the string is not a valid PIN
|
|
27
56
|
#
|
|
28
57
|
# @example
|
|
29
|
-
# Sashite::Pin.
|
|
30
|
-
# Sashite::Pin
|
|
31
|
-
# Sashite::Pin.valid?("-p") # => true
|
|
32
|
-
# Sashite::Pin.valid?("KK") # => false
|
|
33
|
-
# Sashite::Pin.valid?("++K") # => false
|
|
34
|
-
def self.valid?(pin_string)
|
|
35
|
-
Identifier.valid?(pin_string)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Parse a PIN string into an Identifier object
|
|
58
|
+
# Sashite::Pin.parse("K")
|
|
59
|
+
# # => #<Sashite::Pin::Identifier K>
|
|
39
60
|
#
|
|
40
|
-
#
|
|
41
|
-
#
|
|
42
|
-
#
|
|
43
|
-
#
|
|
44
|
-
#
|
|
45
|
-
#
|
|
46
|
-
# Sashite::Pin.parse("
|
|
47
|
-
|
|
48
|
-
|
|
61
|
+
# Sashite::Pin.parse("+r")
|
|
62
|
+
# # => #<Sashite::Pin::Identifier +r>
|
|
63
|
+
#
|
|
64
|
+
# Sashite::Pin.parse("K^")
|
|
65
|
+
# # => #<Sashite::Pin::Identifier K^>
|
|
66
|
+
#
|
|
67
|
+
# Sashite::Pin.parse("invalid")
|
|
68
|
+
# # => raises Errors::Argument
|
|
69
|
+
def self.parse(string)
|
|
70
|
+
components = Parser.parse(string)
|
|
71
|
+
|
|
72
|
+
Identifier.new(
|
|
73
|
+
components[:type],
|
|
74
|
+
components[:side],
|
|
75
|
+
components[:state],
|
|
76
|
+
terminal: components[:terminal]
|
|
77
|
+
)
|
|
49
78
|
end
|
|
50
79
|
|
|
51
|
-
#
|
|
80
|
+
# Checks if a string is a valid PIN notation.
|
|
81
|
+
#
|
|
82
|
+
# @param string [String] The string to validate
|
|
83
|
+
# @return [Boolean] true if valid, false otherwise
|
|
52
84
|
#
|
|
53
|
-
# @param type [Symbol] piece type (:A to :Z)
|
|
54
|
-
# @param side [Symbol] player side (:first or :second)
|
|
55
|
-
# @param state [Symbol] piece state (:normal, :enhanced, or :diminished)
|
|
56
|
-
# @param terminal [Boolean] whether the piece is a terminal piece
|
|
57
|
-
# @return [Pin::Identifier] new identifier instance
|
|
58
|
-
# @raise [ArgumentError] if parameters are invalid
|
|
59
85
|
# @example
|
|
60
|
-
# Sashite::Pin.
|
|
61
|
-
# Sashite::Pin.
|
|
62
|
-
# Sashite::Pin.
|
|
63
|
-
# Sashite::Pin.
|
|
64
|
-
def self.
|
|
65
|
-
|
|
86
|
+
# Sashite::Pin.valid?("K") # => true
|
|
87
|
+
# Sashite::Pin.valid?("+R") # => true
|
|
88
|
+
# Sashite::Pin.valid?("K^") # => true
|
|
89
|
+
# Sashite::Pin.valid?("invalid") # => false
|
|
90
|
+
def self.valid?(string)
|
|
91
|
+
Parser.valid?(string)
|
|
66
92
|
end
|
|
67
93
|
end
|
|
68
94
|
end
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: sashite-pin
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 4.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Cyril Kato
|
|
@@ -9,34 +9,35 @@ bindir: bin
|
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
11
|
dependencies: []
|
|
12
|
-
description:
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
a modern Ruby interface featuring immutable identifier objects and functional programming
|
|
16
|
-
principles. PIN uses single ASCII letters with optional state modifiers, terminal markers,
|
|
17
|
-
and case-based side encoding (A-Z for first player, a-z for second player), enabling
|
|
18
|
-
precise and portable identification of pieces across multiple games. Perfect for game
|
|
19
|
-
engines, board game notation systems, and hybrid gaming platforms requiring compact,
|
|
20
|
-
stateful piece representation.
|
|
12
|
+
description: PIN (Piece Identifier Notation) implementation for Ruby. Provides a rule-agnostic
|
|
13
|
+
format for identifying pieces in abstract strategy board games with immutable identifier
|
|
14
|
+
objects and functional programming principles.
|
|
21
15
|
email: contact@cyril.email
|
|
22
16
|
executables: []
|
|
23
17
|
extensions: []
|
|
24
18
|
extra_rdoc_files: []
|
|
25
19
|
files:
|
|
26
|
-
- LICENSE
|
|
20
|
+
- LICENSE
|
|
27
21
|
- README.md
|
|
28
22
|
- lib/sashite-pin.rb
|
|
29
23
|
- lib/sashite/pin.rb
|
|
24
|
+
- lib/sashite/pin/constants.rb
|
|
25
|
+
- lib/sashite/pin/errors.rb
|
|
26
|
+
- lib/sashite/pin/errors/argument.rb
|
|
27
|
+
- lib/sashite/pin/errors/argument/messages.rb
|
|
30
28
|
- lib/sashite/pin/identifier.rb
|
|
29
|
+
- lib/sashite/pin/parser.rb
|
|
31
30
|
homepage: https://github.com/sashite/pin.rb
|
|
32
31
|
licenses:
|
|
33
|
-
-
|
|
32
|
+
- Apache-2.0
|
|
34
33
|
metadata:
|
|
35
34
|
bug_tracker_uri: https://github.com/sashite/pin.rb/issues
|
|
36
35
|
documentation_uri: https://rubydoc.info/github/sashite/pin.rb/main
|
|
37
36
|
homepage_uri: https://github.com/sashite/pin.rb
|
|
38
37
|
source_code_uri: https://github.com/sashite/pin.rb
|
|
39
38
|
specification_uri: https://sashite.dev/specs/pin/1.0.0/
|
|
39
|
+
wiki_uri: https://sashite.dev/specs/pin/1.0.0/examples/
|
|
40
|
+
funding_uri: https://github.com/sponsors/sashite
|
|
40
41
|
rubygems_mfa_required: 'true'
|
|
41
42
|
rdoc_options: []
|
|
42
43
|
require_paths:
|
|
@@ -52,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
52
53
|
- !ruby/object:Gem::Version
|
|
53
54
|
version: '0'
|
|
54
55
|
requirements: []
|
|
55
|
-
rubygems_version:
|
|
56
|
+
rubygems_version: 4.0.3
|
|
56
57
|
specification_version: 4
|
|
57
58
|
summary: PIN (Piece Identifier Notation) implementation for Ruby with immutable identifier
|
|
58
59
|
objects
|
data/LICENSE.md
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
Copyright (c) 2025 Cyril Kato
|
|
2
|
-
|
|
3
|
-
MIT License
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
|
6
|
-
a copy of this software and associated documentation files (the
|
|
7
|
-
"Software"), to deal in the Software without restriction, including
|
|
8
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
|
9
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
|
10
|
-
permit persons to whom the Software is furnished to do so, subject to
|
|
11
|
-
the following conditions:
|
|
12
|
-
|
|
13
|
-
The above copyright notice and this permission notice shall be
|
|
14
|
-
included in all copies or substantial portions of the Software.
|
|
15
|
-
|
|
16
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
|
17
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
|
18
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
|
19
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
|
20
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
|
21
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
|
22
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|