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.
@@ -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
- # Provides ASCII-based format for representing pieces in abstract strategy board games.
9
- # PIN translates piece attributes from the Game Protocol into a compact, portable notation system.
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: [<state>]<letter>
12
- # - State modifier: "+" (enhanced), "-" (diminished), or none (normal)
13
- # - Letter: A-Z (first player), a-z (second player)
15
+ # == Format
14
16
  #
15
- # Examples:
16
- # "K" - First player king (normal state)
17
- # "k" - Second player king (normal state)
18
- # "+R" - First player rook (enhanced state)
19
- # "-p" - Second player pawn (diminished state)
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
- # Check if a string is a valid PIN notation
51
+ # Parses a PIN string into an Identifier.
24
52
  #
25
- # @param pin_string [String] The string to validate
26
- # @return [Boolean] true if valid PIN, false otherwise
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.valid?("K") # => true
30
- # Sashite::Pin.valid?("+R") # => true
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
- # @param pin_string [String] PIN notation string
41
- # @return [Pin::Identifier] new identifier instance
42
- # @raise [ArgumentError] if the PIN string is invalid
43
- # @example
44
- # Sashite::Pin.parse("K") # => #<Pin::Identifier type=:K side=:first state=:normal>
45
- # Sashite::Pin.parse("+R") # => #<Pin::Identifier type=:R side=:first state=:enhanced>
46
- # Sashite::Pin.parse("-p") # => #<Pin::Identifier type=:P side=:second state=:diminished>
47
- def self.parse(pin_string)
48
- Identifier.parse(pin_string)
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
- # Create a new identifier instance
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.identifier(:K, :first, :normal) # => #<Pin::Identifier type=:K side=:first state=:normal terminal=false>
61
- # Sashite::Pin.identifier(:R, :first, :enhanced) # => #<Pin::Identifier type=:R side=:first state=:enhanced terminal=false>
62
- # Sashite::Pin.identifier(:P, :second, :diminished) # => #<Pin::Identifier type=:P side=:second state=:diminished terminal=false>
63
- # Sashite::Pin.identifier(:K, :first, :normal, terminal: true) # => #<Pin::Identifier type=:K side=:first state=:normal terminal=true>
64
- def self.identifier(type, side, state, terminal: false)
65
- Identifier.new(type, side, state, terminal: terminal)
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: 3.2.0
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
- PIN (Piece Identifier Notation) provides a rule-agnostic format for identifying pieces
14
- in abstract strategy board games. This gem implements the PIN Specification v1.0.0 with
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.md
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
- - MIT
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: 3.7.2
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.