sashite-sin 2.1.0 → 3.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.
@@ -0,0 +1,137 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "constants"
4
+ require_relative "errors"
5
+
6
+ module Sashite
7
+ module Sin
8
+ # Parses SIN (Style Identifier Notation) strings.
9
+ #
10
+ # The parser uses byte-level validation to ensure security against
11
+ # malformed input, Unicode lookalikes, and injection attacks.
12
+ #
13
+ # @example Parsing a valid SIN string
14
+ # Parser.parse("C") # => { abbr: :C, side: :first }
15
+ # Parser.parse("c") # => { abbr: :C, side: :second }
16
+ #
17
+ # @example Validation
18
+ # Parser.valid?("C") # => true
19
+ # Parser.valid?("CC") # => false
20
+ #
21
+ # @see https://sashite.dev/specs/sin/1.0.0/
22
+ module Parser
23
+ # Parses a SIN string into its components.
24
+ #
25
+ # @param input [String] The SIN string to parse
26
+ # @return [Hash] Hash with :abbr and :side keys
27
+ # @raise [Errors::Argument] If the input is invalid
28
+ #
29
+ # @example
30
+ # Parser.parse("C") # => { abbr: :C, side: :first }
31
+ # Parser.parse("s") # => { abbr: :S, side: :second }
32
+ def self.parse(input)
33
+ validate_input_type!(input)
34
+ validate_not_empty!(input)
35
+ validate_length!(input)
36
+
37
+ byte = input.getbyte(0)
38
+ validate_letter!(byte)
39
+
40
+ extract_components(byte)
41
+ end
42
+
43
+ # Reports whether the input is a valid SIN string.
44
+ #
45
+ # @param input [String] The string to validate
46
+ # @return [Boolean] true if valid, false otherwise
47
+ #
48
+ # @example
49
+ # Parser.valid?("C") # => true
50
+ # Parser.valid?("c") # => true
51
+ # Parser.valid?("") # => false
52
+ # Parser.valid?("CC") # => false
53
+ def self.valid?(input)
54
+ parse(input)
55
+ true
56
+ rescue Errors::Argument
57
+ false
58
+ end
59
+
60
+ # @!group Private Class Methods
61
+
62
+ # Validates that input is a String.
63
+ #
64
+ # @param input [Object] The input to validate
65
+ # @raise [Errors::Argument] If input is not a String
66
+ # @return [void]
67
+ private_class_method def self.validate_input_type!(input)
68
+ return if ::String === input
69
+
70
+ raise Errors::Argument, Errors::Argument::Messages::MUST_BE_LETTER
71
+ end
72
+
73
+ # Validates that input is not empty.
74
+ #
75
+ # @param input [String] The input to validate
76
+ # @raise [Errors::Argument] If input is empty
77
+ # @return [void]
78
+ private_class_method def self.validate_not_empty!(input)
79
+ return unless input.empty?
80
+
81
+ raise Errors::Argument, Errors::Argument::Messages::EMPTY_INPUT
82
+ end
83
+
84
+ # Validates that input does not exceed maximum length.
85
+ #
86
+ # @param input [String] The input to validate
87
+ # @raise [Errors::Argument] If input exceeds maximum length
88
+ # @return [void]
89
+ private_class_method def self.validate_length!(input)
90
+ return if input.bytesize <= Constants::MAX_STRING_LENGTH
91
+
92
+ raise Errors::Argument, Errors::Argument::Messages::INPUT_TOO_LONG
93
+ end
94
+
95
+ # Validates that byte is an ASCII letter.
96
+ #
97
+ # @param byte [Integer] The byte to validate
98
+ # @raise [Errors::Argument] If byte is not a letter
99
+ # @return [void]
100
+ private_class_method def self.validate_letter!(byte)
101
+ return if uppercase_letter?(byte) || lowercase_letter?(byte)
102
+
103
+ raise Errors::Argument, Errors::Argument::Messages::MUST_BE_LETTER
104
+ end
105
+
106
+ # Extracts abbr and side from a validated byte.
107
+ #
108
+ # @param byte [Integer] A validated ASCII letter byte
109
+ # @return [Hash] Hash with :abbr and :side keys
110
+ private_class_method def self.extract_components(byte)
111
+ if uppercase_letter?(byte)
112
+ { abbr: byte.chr.to_sym, side: :first }
113
+ else
114
+ { abbr: byte.chr.upcase.to_sym, side: :second }
115
+ end
116
+ end
117
+
118
+ # Reports whether byte is an uppercase ASCII letter (A-Z).
119
+ #
120
+ # @param byte [Integer] The byte to check
121
+ # @return [Boolean] true if A-Z
122
+ private_class_method def self.uppercase_letter?(byte)
123
+ byte >= 0x41 && byte <= 0x5A
124
+ end
125
+
126
+ # Reports whether byte is a lowercase ASCII letter (a-z).
127
+ #
128
+ # @param byte [Integer] The byte to check
129
+ # @return [Boolean] true if a-z
130
+ private_class_method def self.lowercase_letter?(byte)
131
+ byte >= 0x61 && byte <= 0x7A
132
+ end
133
+
134
+ # @!endgroup
135
+ end
136
+ end
137
+ end
data/lib/sashite/sin.rb CHANGED
@@ -1,188 +1,70 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "sin/constants"
4
+ require_relative "sin/errors"
3
5
  require_relative "sin/identifier"
6
+ require_relative "sin/parser"
4
7
 
5
8
  module Sashite
6
- # SIN (Style Identifier Notation) implementation for Ruby
9
+ # SIN (Style Identifier Notation) implementation for Ruby.
7
10
  #
8
- # Provides a compact, ASCII-based format for identifying styles in abstract strategy board games.
9
- # SIN uses single-character identifiers with case encoding to represent both style identity
10
- # and player assignment simultaneously.
11
+ # SIN provides a compact, ASCII-based format for encoding Player Style
12
+ # with Player Side assignment in abstract strategy board games.
11
13
  #
12
- # ## Concept
14
+ # A SIN token is exactly one ASCII letter:
15
+ # - Uppercase (A-Z) indicates first player
16
+ # - Lowercase (a-z) indicates second player
13
17
  #
14
- # SIN addresses the fundamental need to identify which style system governs piece behavior
15
- # while simultaneously indicating which player controls pieces of that style. In cross-style
16
- # scenarios where different players use different game traditions, this dual encoding becomes
17
- # essential for unambiguous piece identification.
18
+ # @example Parsing SIN strings
19
+ # sin = Sashite::Sin.parse("C")
20
+ # sin.abbr # => :C
21
+ # sin.side # => :first
22
+ # sin.to_s # => "C"
18
23
  #
19
- # ## Dual-Purpose Encoding
24
+ # @example Creating identifiers directly
25
+ # sin = Sashite::Sin::Identifier.new(:C, :first)
26
+ # sin.to_s # => "C"
20
27
  #
21
- # Each SIN identifier serves two functions:
22
- # - **Style Family Identification**: The letter choice indicates which rule system applies
23
- # - **Player Assignment**: The letter case indicates which player uses this style as their native system
28
+ # @example Validation
29
+ # Sashite::Sin.valid?("C") # => true
30
+ # Sashite::Sin.valid?("CC") # => false
24
31
  #
25
- # ## Format Specification
26
- #
27
- # Structure: `<style-letter>`
28
- #
29
- # Grammar (BNF):
30
- # <sin> ::= <uppercase-letter> | <lowercase-letter>
31
- # <uppercase-letter> ::= "A" | "B" | "C" | ... | "Z"
32
- # <lowercase-letter> ::= "a" | "b" | "c" | ... | "z"
33
- #
34
- # Regular Expression: `/\A[A-Za-z]\z/`
35
- #
36
- # ## Style Attribute Mapping
37
- #
38
- # SIN encodes style attributes using the following correspondence:
39
- #
40
- # | Style Attribute | SIN Encoding | Examples |
41
- # |-----------------|--------------|----------|
42
- # | **Letter** | Single ASCII character | `C`, `c`, `S`, `s` |
43
- # | **Style Family** | ASCII letter choice (A-Z) | `C`/`c` = Chess, `S`/`s` = Shōgi |
44
- # | **Player Assignment** | Letter case | `C` = First player, `c` = Second player |
45
- #
46
- # The **Letter** attribute combines two distinct semantic components:
47
- # - **Style Family**: The underlying ASCII character (A-Z), representing the game tradition or rule system
48
- # - **Player Assignment**: The case of the character (uppercase/lowercase), representing which player uses this style
49
- #
50
- # ## Character Selection Conventions
51
- #
52
- # ### Primary Convention: First Letter
53
- # By convention, SIN identifiers should preferably use the **first letter** of the corresponding SNN style name:
54
- # - `Chess` → `C`/`c`
55
- # - `Shogi` → `S`/`s`
56
- # - `Xiangqi` → `X`/`x`
57
- # - `Makruk` → `M`/`m`
58
- # - `Janggi` → `J`/`j`
59
- #
60
- # ### Collision Resolution
61
- # When multiple styles would claim the same first letter, systematic collision resolution applies
62
- # using sequential letter selection from the SNN name until a unique identifier is found.
63
- #
64
- # ### Compatibility Groups
65
- # Styles requiring incompatible board structures can safely share SIN letters since they
66
- # cannot coexist in the same match.
67
- #
68
- # ## System Constraints
69
- #
70
- # - **26 possible identifiers** per player using ASCII letters
71
- # - **Exactly 2 players** through case distinction:
72
- # - First player: Uppercase letters (`A-Z`)
73
- # - Second player: Lowercase letters (`a-z`)
74
- # - **Single character** per style-player combination
75
- # - **Rule-agnostic** - independent of specific game mechanics
76
- #
77
- # ## Examples
78
- #
79
- # ### Traditional Game Styles
80
- #
81
- # # Chess (8×8)
82
- # chess_white = Sashite::Sin.parse("C") # First player (White pieces)
83
- # chess_black = Sashite::Sin.parse("c") # Second player (Black pieces)
84
- #
85
- # # Shōgi (9×9)
86
- # shogi_sente = Sashite::Sin.parse("S") # First player (Sente 先手)
87
- # shogi_gote = Sashite::Sin.parse("s") # Second player (Gote 後手)
88
- #
89
- # # Xiangqi (9×10)
90
- # xiangqi_red = Sashite::Sin.parse("X") # First player (Red pieces)
91
- # xiangqi_black = Sashite::Sin.parse("x") # Second player (Black pieces)
92
- #
93
- # ### Cross-Style Scenarios
94
- #
95
- # # Chess vs. Ōgi Match (both 8×8 compatible)
96
- # chess_style = Sashite::Sin.parse("C") # Chess style, first player
97
- # ogi_style = Sashite::Sin.parse("o") # Ōgi style, second player
98
- #
99
- # ### All 26 Letters
100
- #
101
- # # First player identifiers (A-Z)
102
- # ("A".."Z").each { |letter| Sashite::Sin.parse(letter).first_player? } # => all true
103
- #
104
- # # Second player identifiers (a-z)
105
- # ("a".."z").each { |letter| Sashite::Sin.parse(letter).second_player? } # => all true
106
- #
107
- # ## Design Properties
108
- #
109
- # - **ASCII compatibility**: Maximum portability across systems
110
- # - **Rule-agnostic**: Independent of specific game mechanics
111
- # - **Minimal overhead**: Single character per style-player combination
112
- # - **Flexible collision resolution**: Systematic approaches for identifier conflicts
113
- # - **Semantic clarity**: Distinct concepts for Letter (Style Family + Player Assignment)
114
- # - **SNN coordination**: Works harmoniously with formal style naming
115
- # - **Context-aware**: Adapts to avoid conflicts within specific game scenarios
116
- #
117
- # @see https://sashite.dev/specs/sin/1.0.0/ SIN Specification v1.0.0
118
- # @see https://sashite.dev/specs/sin/1.0.0/examples/ SIN Examples
119
- # @see https://sashite.dev/specs/snn/ Style Name Notation (SNN)
32
+ # @see https://sashite.dev/specs/sin/1.0.0/
120
33
  module Sin
121
- # Check if a string is a valid SIN notation
34
+ # Parses a SIN string into an Identifier.
122
35
  #
123
- # @param sin_string [String] the string to validate
124
- # @return [Boolean] true if valid SIN, false otherwise
36
+ # @param input [String] The SIN string to parse
37
+ # @return [Identifier] The parsed identifier
38
+ # @raise [Errors::Argument] If the input is invalid
125
39
  #
126
- # @example Validate various SIN formats
127
- # Sashite::Sin.valid?("C") # => true (Chess first player)
128
- # Sashite::Sin.valid?("c") # => true (Chess second player)
129
- # Sashite::Sin.valid?("S") # => true (Shōgi first player)
130
- # Sashite::Sin.valid?("s") # => true (Shōgi second player)
131
- # Sashite::Sin.valid?("CHESS") # => false (multi-character)
132
- # Sashite::Sin.valid?("1") # => false (not a letter)
133
- # Sashite::Sin.valid?("") # => false (empty string)
134
- def self.valid?(sin_string)
135
- Identifier.valid?(sin_string)
136
- end
137
-
138
- # Parse an SIN string into an Identifier object
139
- #
140
- # The identifier will have both letter and side attributes inferred from the case:
141
- # - Uppercase letter → first player (:first)
142
- # - Lowercase letter → second player (:second)
143
- #
144
- # @param sin_string [String] SIN notation string (single ASCII letter)
145
- # @return [Sin::Identifier] parsed identifier object with letter and side attributes
146
- # @raise [ArgumentError] if the SIN string is invalid
147
- #
148
- # @example Parse different SIN formats with dual-purpose encoding
149
- # Sashite::Sin.parse("C") # => #<Sashite::Sin::Identifier @family=:C, @side=:first>
150
- # Sashite::Sin.parse("c") # => #<Sashite::Sin::Identifier @family=:C, @side=:second>
151
- # Sashite::Sin.parse("S") # => #<Sashite::Sin::Identifier @family=:S, @side=:first>
152
- # Sashite::Sin.parse("s") # => #<Sashite::Sin::Identifier @family=:S, @side=:second>
40
+ # @example Parsing uppercase (first player)
41
+ # sin = Sashite::Sin.parse("C")
42
+ # sin.abbr # => :C
43
+ # sin.side # => :first
153
44
  #
154
- # @example Traditional game styles
155
- # chess_white = Sashite::Sin.parse("C") # Chess, first player (White)
156
- # chess_black = Sashite::Sin.parse("c") # Chess, second player (Black)
157
- # shogi_sente = Sashite::Sin.parse("S") # Shōgi, first player (Sente)
158
- # shogi_gote = Sashite::Sin.parse("s") # Shōgi, second player (Gote)
159
- def self.parse(sin_string)
160
- Identifier.parse(sin_string)
45
+ # @example Parsing lowercase (second player)
46
+ # sin = Sashite::Sin.parse("c")
47
+ # sin.abbr # => :C
48
+ # sin.side # => :second
49
+ def self.parse(input)
50
+ components = Parser.parse(input)
51
+
52
+ Identifier.new(components.fetch(:abbr), components.fetch(:side))
161
53
  end
162
54
 
163
- # Create a new identifier instance with canonical representation
164
- #
165
- # Ensures the letter case matches the specified side:
166
- # - :first side → uppercase letter
167
- # - :second side → lowercase letter
168
- #
169
- # @param family [Symbol] style family (:A to :Z representing Style Family)
170
- # @param side [Symbol] player side (:first or :second)
171
- # @return [Sin::Identifier] new immutable identifier instance
172
- # @raise [ArgumentError] if parameters are invalid
173
- #
174
- # @example Create identifiers with family and side separation
175
- # Sashite::Sin.identifier(:C, :first) # => #<Sashite::Sin::Identifier @family=:C, @side=:first>
176
- # Sashite::Sin.identifier(:C, :second) # => #<Sashite::Sin::Identifier @family=:C, @side=:second>
55
+ # Reports whether the input is a valid SIN string.
177
56
  #
178
- # @example Style family and player assignment
179
- # chess_first = Sashite::Sin.identifier(:C, :first) # Chess family, first player
180
- # chess_second = Sashite::Sin.identifier(:C, :second) # Chess family, second player
57
+ # @param input [String] The string to validate
58
+ # @return [Boolean] true if valid, false otherwise
181
59
  #
182
- # chess_first.same_family?(chess_second) # => true (same style family)
183
- # chess_first.same_side?(chess_second) # => false (different players)
184
- def self.identifier(family, side)
185
- Identifier.new(family, side)
60
+ # @example
61
+ # Sashite::Sin.valid?("C") # => true
62
+ # Sashite::Sin.valid?("c") # => true
63
+ # Sashite::Sin.valid?("") # => false
64
+ # Sashite::Sin.valid?("CC") # => false
65
+ # Sashite::Sin.valid?("1") # => false
66
+ def self.valid?(input)
67
+ Parser.valid?(input)
186
68
  end
187
69
  end
188
70
  end
data/lib/sashite-sin.rb CHANGED
@@ -1,14 +1,3 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "sashite/sin"
4
-
5
- # Sashité namespace for board game notation libraries
6
- #
7
- # Sashité provides a collection of libraries for representing and manipulating
8
- # board game concepts according to the Sashité Protocol specifications.
9
- #
10
- # @see https://sashite.dev/protocol/ Sashité Protocol
11
- # @see https://sashite.dev/specs/ Sashité Specifications
12
- # @author Sashité
13
- module Sashite
14
- end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: sashite-sin
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 3.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Cyril Kato
@@ -9,33 +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
- SIN (Style Identifier Notation) provides a rule-agnostic format for identifying styles
14
- in abstract strategy board games. This gem implements the SIN Specification v1.0.0 with
15
- a modern Ruby interface featuring immutable identifier objects and functional programming
16
- principles. SIN uses single ASCII letters with case-based side encoding (A-Z for first player,
17
- a-z for second player), enabling clear distinction between different style families in
18
- multi-style gaming environments. Perfect for cross-style matches, game engines, and hybrid
19
- gaming systems requiring compact style identification with enhanced collision resolution.
12
+ description: SIN (Style Identifier Notation) implementation for Ruby. Provides a rule-agnostic
13
+ format for identifying player styles in abstract strategy board games with immutable
14
+ identifier objects and functional programming principles.
20
15
  email: contact@cyril.email
21
16
  executables: []
22
17
  extensions: []
23
18
  extra_rdoc_files: []
24
19
  files:
25
- - LICENSE.md
20
+ - LICENSE
26
21
  - README.md
27
22
  - lib/sashite-sin.rb
28
23
  - lib/sashite/sin.rb
24
+ - lib/sashite/sin/constants.rb
25
+ - lib/sashite/sin/errors.rb
26
+ - lib/sashite/sin/errors/argument.rb
27
+ - lib/sashite/sin/errors/argument/messages.rb
29
28
  - lib/sashite/sin/identifier.rb
29
+ - lib/sashite/sin/parser.rb
30
30
  homepage: https://github.com/sashite/sin.rb
31
31
  licenses:
32
- - MIT
32
+ - Apache-2.0
33
33
  metadata:
34
34
  bug_tracker_uri: https://github.com/sashite/sin.rb/issues
35
35
  documentation_uri: https://rubydoc.info/github/sashite/sin.rb/main
36
36
  homepage_uri: https://github.com/sashite/sin.rb
37
37
  source_code_uri: https://github.com/sashite/sin.rb
38
38
  specification_uri: https://sashite.dev/specs/sin/1.0.0/
39
+ wiki_uri: https://sashite.dev/specs/sin/1.0.0/examples/
40
+ funding_uri: https://github.com/sponsors/sashite
39
41
  rubygems_mfa_required: 'true'
40
42
  rdoc_options: []
41
43
  require_paths:
@@ -51,7 +53,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
51
53
  - !ruby/object:Gem::Version
52
54
  version: '0'
53
55
  requirements: []
54
- rubygems_version: 3.6.9
56
+ rubygems_version: 4.0.3
55
57
  specification_version: 4
56
58
  summary: SIN (Style Identifier Notation) implementation for Ruby with immutable identifier
57
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.