sashite-ggn 0.1.0 → 0.3.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 +5 -5
- data/LICENSE.md +17 -18
- data/README.md +356 -506
- data/lib/sashite/ggn/piece/source/destination/engine/transition.rb +90 -0
- data/lib/sashite/ggn/piece/source/destination/engine.rb +407 -0
- data/lib/sashite/ggn/piece/source/destination.rb +65 -0
- data/lib/sashite/ggn/piece/source.rb +71 -0
- data/lib/sashite/ggn/piece.rb +77 -0
- data/lib/sashite/ggn/schema.rb +152 -0
- data/lib/sashite/ggn/validation_error.rb +31 -0
- data/lib/sashite/ggn.rb +317 -5
- data/lib/sashite-ggn.rb +112 -1
- metadata +31 -151
- data/.gitignore +0 -22
- data/.ruby-version +0 -1
- data/.travis.yml +0 -3
- data/Gemfile +0 -2
- data/Rakefile +0 -7
- data/VERSION.semver +0 -1
- data/lib/sashite/ggn/ability.rb +0 -29
- data/lib/sashite/ggn/actor.rb +0 -20
- data/lib/sashite/ggn/ally.rb +0 -20
- data/lib/sashite/ggn/area.rb +0 -17
- data/lib/sashite/ggn/attacked.rb +0 -20
- data/lib/sashite/ggn/boolean.rb +0 -17
- data/lib/sashite/ggn/digit.rb +0 -20
- data/lib/sashite/ggn/digit_excluding_zero.rb +0 -17
- data/lib/sashite/ggn/direction.rb +0 -19
- data/lib/sashite/ggn/gameplay.rb +0 -20
- data/lib/sashite/ggn/gameplay_into_base64.rb +0 -21
- data/lib/sashite/ggn/integer.rb +0 -20
- data/lib/sashite/ggn/last_moved_actor.rb +0 -20
- data/lib/sashite/ggn/maximum_magnitude.rb +0 -20
- data/lib/sashite/ggn/name.rb +0 -17
- data/lib/sashite/ggn/negative_integer.rb +0 -19
- data/lib/sashite/ggn/null.rb +0 -21
- data/lib/sashite/ggn/object.rb +0 -28
- data/lib/sashite/ggn/occupied.rb +0 -29
- data/lib/sashite/ggn/pattern.rb +0 -20
- data/lib/sashite/ggn/previous_moves_counter.rb +0 -20
- data/lib/sashite/ggn/promotable_into_actors.rb +0 -23
- data/lib/sashite/ggn/required.rb +0 -19
- data/lib/sashite/ggn/self.rb +0 -21
- data/lib/sashite/ggn/square.rb +0 -29
- data/lib/sashite/ggn/state.rb +0 -26
- data/lib/sashite/ggn/subject.rb +0 -29
- data/lib/sashite/ggn/unsigned_integer.rb +0 -20
- data/lib/sashite/ggn/unsigned_integer_excluding_zero.rb +0 -20
- data/lib/sashite/ggn/verb.rb +0 -33
- data/lib/sashite/ggn/zero.rb +0 -21
- data/sashite-ggn.gemspec +0 -19
- data/test/_test_helper.rb +0 -2
- data/test/test_ggn.rb +0 -552
- data/test/test_ggn_ability.rb +0 -51
- data/test/test_ggn_actor.rb +0 -571
- data/test/test_ggn_ally.rb +0 -35
- data/test/test_ggn_area.rb +0 -21
- data/test/test_ggn_attacked.rb +0 -35
- data/test/test_ggn_boolean.rb +0 -21
- data/test/test_ggn_digit.rb +0 -21
- data/test/test_ggn_digit_excluding_zero.rb +0 -21
- data/test/test_ggn_direction.rb +0 -21
- data/test/test_ggn_gameplay.rb +0 -557
- data/test/test_ggn_gameplay_into_base64.rb +0 -555
- data/test/test_ggn_integer.rb +0 -39
- data/test/test_ggn_last_moved_actor.rb +0 -35
- data/test/test_ggn_maximum_magnitude.rb +0 -39
- data/test/test_ggn_name.rb +0 -21
- data/test/test_ggn_negative_integer.rb +0 -21
- data/test/test_ggn_null.rb +0 -21
- data/test/test_ggn_object.rb +0 -33
- data/test/test_ggn_occupied.rb +0 -78
- data/test/test_ggn_pattern.rb +0 -84
- data/test/test_ggn_previous_moves_counter.rb +0 -39
- data/test/test_ggn_promotable_into_actors.rb +0 -578
- data/test/test_ggn_required.rb +0 -21
- data/test/test_ggn_self.rb +0 -21
- data/test/test_ggn_square.rb +0 -25
- data/test/test_ggn_state.rb +0 -24
- data/test/test_ggn_subject.rb +0 -28
- data/test/test_ggn_unsigned_integer.rb +0 -39
- data/test/test_ggn_unsigned_integer_excluding_zero.rb +0 -25
- data/test/test_ggn_verb.rb +0 -27
- data/test/test_ggn_zero.rb +0 -21
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative File.join("piece", "source")
|
4
|
+
|
5
|
+
module Sashite
|
6
|
+
module Ggn
|
7
|
+
# Represents a collection of piece definitions from a GGN document.
|
8
|
+
#
|
9
|
+
# A Piece instance contains all the pseudo-legal move definitions for
|
10
|
+
# various game pieces, organized by their GAN (General Actor Notation)
|
11
|
+
# identifiers. This class provides the entry point for querying specific
|
12
|
+
# piece movement rules.
|
13
|
+
#
|
14
|
+
# @example Basic usage
|
15
|
+
# piece_data = Sashite::Ggn.load_file('chess.json')
|
16
|
+
# chess_king = piece_data.select('CHESS:K')
|
17
|
+
# shogi_pawn = piece_data.select('SHOGI:P')
|
18
|
+
#
|
19
|
+
# @example Complete workflow
|
20
|
+
# piece_data = Sashite::Ggn.load_file('game_moves.json')
|
21
|
+
#
|
22
|
+
# # Query specific piece moves
|
23
|
+
# begin
|
24
|
+
# king_source = piece_data.select('CHESS:K')
|
25
|
+
# puts "Found chess king movement rules"
|
26
|
+
# rescue KeyError
|
27
|
+
# puts "Chess king not found in this dataset"
|
28
|
+
# end
|
29
|
+
#
|
30
|
+
# @see https://sashite.dev/documents/gan/ GAN Specification
|
31
|
+
class Piece
|
32
|
+
# Creates a new Piece instance from GGN data.
|
33
|
+
#
|
34
|
+
# @param data [Hash] The parsed GGN JSON data structure, where keys are
|
35
|
+
# GAN identifiers and values contain the movement definitions.
|
36
|
+
#
|
37
|
+
# @raise [ArgumentError] If data is not a Hash
|
38
|
+
def initialize(data)
|
39
|
+
raise ::ArgumentError, "Expected Hash, got #{data.class}" unless data.is_a?(::Hash)
|
40
|
+
|
41
|
+
@data = data
|
42
|
+
|
43
|
+
freeze
|
44
|
+
end
|
45
|
+
|
46
|
+
# Retrieves movement rules for a specific piece type.
|
47
|
+
#
|
48
|
+
# @param actor [String] The GAN identifier for the piece type
|
49
|
+
# (e.g., 'CHESS:K', 'SHOGI:P', 'chess:q'). Must match exactly
|
50
|
+
# including case sensitivity.
|
51
|
+
#
|
52
|
+
# @return [Source] A Source instance containing all movement rules
|
53
|
+
# for this piece type from different board positions.
|
54
|
+
#
|
55
|
+
# @raise [KeyError] If the actor is not found in the GGN data
|
56
|
+
#
|
57
|
+
# @example Fetching chess king moves
|
58
|
+
# source = piece_data.select('CHESS:K')
|
59
|
+
# destinations = source.from('e1')
|
60
|
+
# engine = destinations.to('e2')
|
61
|
+
#
|
62
|
+
# @example Handling missing pieces
|
63
|
+
# begin
|
64
|
+
# moves = piece_data.select('NONEXISTENT:X')
|
65
|
+
# rescue KeyError => e
|
66
|
+
# puts "Piece not found: #{e.message}"
|
67
|
+
# end
|
68
|
+
#
|
69
|
+
# @note The actor format must follow GAN specification:
|
70
|
+
# GAME:piece (e.g., CHESS:K, shogi:p, XIANGQI:E)
|
71
|
+
def select(actor)
|
72
|
+
data = @data.fetch(actor)
|
73
|
+
Source.new(data, actor:)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,152 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Ggn
|
5
|
+
# JSON Schema for General Gameplay Notation (GGN) validation.
|
6
|
+
#
|
7
|
+
# This schema defines the structure and constraints for GGN documents,
|
8
|
+
# which describe pseudo-legal moves in abstract strategy board games.
|
9
|
+
# GGN is rule-agnostic and focuses on basic movement constraints rather
|
10
|
+
# than game-specific legality (e.g., check, ko, repetition).
|
11
|
+
#
|
12
|
+
# @example Basic GGN document structure
|
13
|
+
# {
|
14
|
+
# "CHESS:K": {
|
15
|
+
# "e1": {
|
16
|
+
# "e2": [
|
17
|
+
# {
|
18
|
+
# "require": { "e2": "empty" },
|
19
|
+
# "perform": { "e1": null, "e2": "CHESS:K" }
|
20
|
+
# }
|
21
|
+
# ]
|
22
|
+
# }
|
23
|
+
# }
|
24
|
+
# }
|
25
|
+
#
|
26
|
+
# @example Complex move with capture and piece gain
|
27
|
+
# {
|
28
|
+
# "OGI:P": {
|
29
|
+
# "e4": {
|
30
|
+
# "e5": [
|
31
|
+
# {
|
32
|
+
# "require": { "e5": "enemy" },
|
33
|
+
# "perform": { "e4": null, "e5": "OGI:P" },
|
34
|
+
# "gain": "OGI:P"
|
35
|
+
# }
|
36
|
+
# ]
|
37
|
+
# }
|
38
|
+
# }
|
39
|
+
# }
|
40
|
+
#
|
41
|
+
# @see https://sashite.dev/documents/ggn/1.0.0/ GGN Specification
|
42
|
+
# @see https://sashite.dev/schemas/ggn/1.0.0/schema.json JSON Schema URL
|
43
|
+
Schema = {
|
44
|
+
# JSON Schema meta-information
|
45
|
+
"$schema": "https://json-schema.org/draft/2020-12/schema",
|
46
|
+
"$id": "https://sashite.dev/schemas/ggn/1.0.0/schema.json",
|
47
|
+
"title": "General Gameplay Notation (GGN)",
|
48
|
+
"description": "JSON Schema for pseudo-legal moves in abstract board games using the GGN format.",
|
49
|
+
"type": "object",
|
50
|
+
|
51
|
+
# Optional schema reference property
|
52
|
+
"properties": {
|
53
|
+
# Allows documents to self-reference the schema
|
54
|
+
"$schema": {
|
55
|
+
"type": "string",
|
56
|
+
"format": "uri"
|
57
|
+
}
|
58
|
+
},
|
59
|
+
|
60
|
+
# Pattern-based validation for GAN (General Actor Notation) identifiers
|
61
|
+
# Matches format: GAME:piece_char (e.g., "CHESS:K'", "shogi:+p", "XIANGQI:E")
|
62
|
+
"patternProperties": {
|
63
|
+
# GAN pattern: game identifier (with casing) + colon + piece identifier
|
64
|
+
# Supports prefixes (-/+), suffixes ('), and both uppercase/lowercase games
|
65
|
+
"^([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$": {
|
66
|
+
"type": "object",
|
67
|
+
"minProperties": 1,
|
68
|
+
|
69
|
+
# Source squares: where the piece starts (or "*" for drops)
|
70
|
+
"additionalProperties": {
|
71
|
+
"type": "object",
|
72
|
+
"minProperties": 1,
|
73
|
+
|
74
|
+
# Destination squares: where the piece can move to
|
75
|
+
"additionalProperties": {
|
76
|
+
"type": "array",
|
77
|
+
"minItems": 0,
|
78
|
+
|
79
|
+
# Array of conditional transitions for this source->destination pair
|
80
|
+
"items": {
|
81
|
+
"type": "object",
|
82
|
+
"properties": {
|
83
|
+
# Conditions that MUST be satisfied before the move (logical AND)
|
84
|
+
"require": {
|
85
|
+
"type": "object",
|
86
|
+
"minProperties": 1,
|
87
|
+
"additionalProperties": {
|
88
|
+
"type": "string",
|
89
|
+
# Occupation states: "empty", "enemy", or exact GAN identifier
|
90
|
+
"pattern": "^empty$|^enemy$|([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
|
91
|
+
}
|
92
|
+
},
|
93
|
+
|
94
|
+
# Conditions that MUST NOT be satisfied before the move (logical OR)
|
95
|
+
"prevent": {
|
96
|
+
"type": "object",
|
97
|
+
"minProperties": 1,
|
98
|
+
"additionalProperties": {
|
99
|
+
"type": "string",
|
100
|
+
# Same occupation states as require
|
101
|
+
"pattern": "^empty$|^enemy$|([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
|
102
|
+
}
|
103
|
+
},
|
104
|
+
|
105
|
+
# Board state changes after the move (REQUIRED field)
|
106
|
+
"perform": {
|
107
|
+
"type": "object",
|
108
|
+
"minProperties": 1,
|
109
|
+
"additionalProperties": {
|
110
|
+
"anyOf": [
|
111
|
+
{
|
112
|
+
# Square contains a piece (GAN identifier)
|
113
|
+
"type": "string",
|
114
|
+
"pattern": "^([A-Z]+:[-+]?[A-Z][']?|[a-z]+:[-+]?[a-z][']?)$"
|
115
|
+
},
|
116
|
+
{
|
117
|
+
# Square becomes empty (null)
|
118
|
+
"type": "null"
|
119
|
+
}
|
120
|
+
]
|
121
|
+
}
|
122
|
+
},
|
123
|
+
|
124
|
+
# Piece added to player's hand (base GAN only, no modifiers)
|
125
|
+
"gain": {
|
126
|
+
"type": ["string", "null"],
|
127
|
+
# Base form GAN pattern (no prefixes/suffixes for hand pieces)
|
128
|
+
"pattern": "^([A-Z]+:[A-Z]|[a-z]+:[a-z])$"
|
129
|
+
},
|
130
|
+
|
131
|
+
# Piece removed from player's hand (base GAN only, no modifiers)
|
132
|
+
"drop": {
|
133
|
+
"type": ["string", "null"],
|
134
|
+
# Base form GAN pattern (no prefixes/suffixes for hand pieces)
|
135
|
+
"pattern": "^([A-Z]+:[A-Z]|[a-z]+:[a-z])$"
|
136
|
+
}
|
137
|
+
},
|
138
|
+
|
139
|
+
# Only "perform" is mandatory; other fields are optional
|
140
|
+
"required": ["perform"],
|
141
|
+
"additionalProperties": false
|
142
|
+
}
|
143
|
+
}
|
144
|
+
}
|
145
|
+
}
|
146
|
+
},
|
147
|
+
|
148
|
+
# No additional properties allowed at root level (strict validation)
|
149
|
+
"additionalProperties": false
|
150
|
+
}.freeze
|
151
|
+
end
|
152
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Sashite
|
4
|
+
module Ggn
|
5
|
+
# Custom exception class for GGN validation and processing errors.
|
6
|
+
#
|
7
|
+
# This exception is raised when GGN documents fail validation against
|
8
|
+
# the JSON Schema, contain malformed data, or encounter processing errors
|
9
|
+
# during parsing and evaluation of pseudo-legal moves.
|
10
|
+
#
|
11
|
+
# Common scenarios that raise ValidationError:
|
12
|
+
# - Invalid JSON syntax in GGN files
|
13
|
+
# - Schema validation failures (missing required fields, invalid patterns)
|
14
|
+
# - File system errors (file not found, permission denied)
|
15
|
+
# - Malformed GAN identifiers or square labels
|
16
|
+
# - Logical contradictions in require/prevent conditions
|
17
|
+
#
|
18
|
+
# @example Handling validation errors during file loading
|
19
|
+
# begin
|
20
|
+
# piece = Sashite::Ggn.load_file('invalid_moves.json')
|
21
|
+
# rescue Sashite::Ggn::ValidationError => e
|
22
|
+
# puts "GGN validation failed: #{e.message}"
|
23
|
+
# # Handle the error appropriately
|
24
|
+
# end
|
25
|
+
#
|
26
|
+
# @see Sashite::Ggn.load_file Main method that can raise this exception
|
27
|
+
# @see Sashite::Ggn::Schema JSON Schema used for validation
|
28
|
+
class ValidationError < ::StandardError
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
data/lib/sashite/ggn.rb
CHANGED
@@ -1,10 +1,322 @@
|
|
1
|
-
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
require 'json_schemer'
|
5
|
+
require 'pathname'
|
6
|
+
|
7
|
+
require_relative File.join("ggn", "piece")
|
8
|
+
require_relative File.join("ggn", "schema")
|
9
|
+
require_relative File.join("ggn", "validation_error")
|
2
10
|
|
3
11
|
module Sashite
|
4
|
-
module
|
5
|
-
|
6
|
-
|
7
|
-
|
12
|
+
# General Gameplay Notation (GGN) module for parsing, validating, and working with
|
13
|
+
# JSON documents that describe pseudo-legal moves in abstract strategy board games.
|
14
|
+
#
|
15
|
+
# GGN is a rule-agnostic format that focuses on basic movement constraints rather
|
16
|
+
# than game-specific legality rules. It answers the fundamental question: "Can this
|
17
|
+
# piece, currently on this square, reach that square?" while remaining neutral about
|
18
|
+
# higher-level game rules like check, ko, repetition, or castling paths.
|
19
|
+
#
|
20
|
+
# ## Key Features
|
21
|
+
#
|
22
|
+
# - **Rule-agnostic**: Works with any abstract strategy board game
|
23
|
+
# - **Pseudo-legal focus**: Describes basic movement constraints only
|
24
|
+
# - **JSON-based**: Structured, machine-readable format
|
25
|
+
# - **Validation support**: Built-in schema validation
|
26
|
+
# - **Performance optimized**: Optional validation for large datasets
|
27
|
+
# - **Cross-game compatible**: Supports hybrid games and variants
|
28
|
+
#
|
29
|
+
# ## Related Specifications
|
30
|
+
#
|
31
|
+
# GGN works alongside other Sashité specifications:
|
32
|
+
# - **GAN** (General Actor Notation): Unique piece identifiers
|
33
|
+
# - **FEEN** (Forsyth-Edwards Enhanced Notation): Board position representation
|
34
|
+
# - **PMN** (Portable Move Notation): Move sequence representation
|
35
|
+
#
|
36
|
+
# @author Sashité <https://sashite.com/>
|
37
|
+
# @version 1.0.0
|
38
|
+
# @see https://sashite.dev/documents/ggn/1.0.0/ Official GGN Specification
|
39
|
+
# @see https://sashite.dev/schemas/ggn/1.0.0/schema.json JSON Schema
|
40
|
+
module Ggn
|
41
|
+
class << self
|
42
|
+
# Loads and validates a GGN JSON file from the filesystem.
|
43
|
+
#
|
44
|
+
# This method provides a complete pipeline for loading GGN data:
|
45
|
+
# 1. Reads the JSON file from the filesystem with proper encoding
|
46
|
+
# 2. Parses the JSON content into a Ruby Hash with error handling
|
47
|
+
# 3. Optionally validates the structure against the GGN JSON Schema
|
48
|
+
# 4. Creates and returns a Piece instance for querying moves
|
49
|
+
#
|
50
|
+
# @param filepath [String, Pathname] Path to the GGN JSON file to load.
|
51
|
+
# Supports both relative and absolute paths.
|
52
|
+
# @param validate [Boolean] Whether to validate against GGN schema (default: true).
|
53
|
+
# Set to false to skip validation for improved performance on large documents.
|
54
|
+
# @param encoding [String] File encoding to use when reading (default: 'UTF-8').
|
55
|
+
# Most GGN files should use UTF-8 encoding.
|
56
|
+
#
|
57
|
+
# @return [Piece] A Piece instance containing the parsed and validated GGN data.
|
58
|
+
# Use this instance to query pseudo-legal moves for specific pieces and positions.
|
59
|
+
#
|
60
|
+
# @raise [ValidationError] If any of the following conditions occur:
|
61
|
+
# - File doesn't exist or cannot be read
|
62
|
+
# - File contains invalid JSON syntax
|
63
|
+
# - File permissions prevent reading
|
64
|
+
# - When validation is enabled: data doesn't conform to GGN schema
|
65
|
+
#
|
66
|
+
# @example Loading a chess piece definition with full validation
|
67
|
+
# begin
|
68
|
+
# piece_data = Sashite::Ggn.load_file('data/chess_pieces.json')
|
69
|
+
# chess_king_source = piece_data.fetch('CHESS:K')
|
70
|
+
# puts "Loaded chess king movement rules successfully"
|
71
|
+
# rescue Sashite::Ggn::ValidationError => e
|
72
|
+
# puts "Failed to load chess pieces: #{e.message}"
|
73
|
+
# end
|
74
|
+
#
|
75
|
+
# @example Complete workflow with move evaluation
|
76
|
+
# begin
|
77
|
+
# piece_data = Sashite::Ggn.load_file('data/chess.json')
|
78
|
+
# source = piece_data.fetch('CHESS:K')
|
79
|
+
# destinations = source.fetch('e1')
|
80
|
+
# engine = destinations.fetch('e2')
|
81
|
+
#
|
82
|
+
# board_state = { 'e1' => 'CHESS:K', 'e2' => nil }
|
83
|
+
# result = engine.evaluate(board_state, {}, 'CHESS')
|
84
|
+
# puts "King can move from e1 to e2" if result
|
85
|
+
# rescue Sashite::Ggn::ValidationError => e
|
86
|
+
# puts "Failed to process move: #{e.message}"
|
87
|
+
# end
|
88
|
+
#
|
89
|
+
# @example Loading large datasets without validation for performance
|
90
|
+
# begin
|
91
|
+
# # Skip validation for large files to improve loading performance
|
92
|
+
# large_dataset = Sashite::Ggn.load_file('data/all_variants.json', validate: false)
|
93
|
+
# puts "Loaded GGN data without validation"
|
94
|
+
# rescue Sashite::Ggn::ValidationError => e
|
95
|
+
# puts "Failed to load dataset: #{e.message}"
|
96
|
+
# end
|
97
|
+
#
|
98
|
+
# @example Handling different file encodings
|
99
|
+
# # Load a GGN file with specific encoding
|
100
|
+
# piece_data = Sashite::Ggn.load_file('legacy_data.json', encoding: 'ISO-8859-1')
|
101
|
+
#
|
102
|
+
# @note Performance Considerations
|
103
|
+
# For large GGN files (>1MB), consider setting validate: false to improve
|
104
|
+
# loading performance. However, this comes with the risk of processing
|
105
|
+
# malformed data. In production environments, validate at least once
|
106
|
+
# before deploying with validation disabled.
|
107
|
+
#
|
108
|
+
# @note Thread Safety
|
109
|
+
# This method is thread-safe for concurrent reads of different files.
|
110
|
+
# However, avoid concurrent access to the same file if it might be
|
111
|
+
# modified during reading.
|
112
|
+
def load_file(filepath, validate: true, encoding: 'UTF-8')
|
113
|
+
# Convert to Pathname for consistent file operations and better error handling
|
114
|
+
file_path = normalize_filepath(filepath)
|
115
|
+
|
116
|
+
# Validate file accessibility before attempting to read
|
117
|
+
validate_file_access(file_path)
|
118
|
+
|
119
|
+
# Parse JSON content with comprehensive error handling
|
120
|
+
data = parse_json_file(file_path, encoding)
|
121
|
+
|
122
|
+
# Validate against GGN schema if requested
|
123
|
+
validate_schema(data, file_path) if validate
|
124
|
+
|
125
|
+
# Create and return Piece instance
|
126
|
+
Piece.new(data)
|
127
|
+
end
|
128
|
+
|
129
|
+
# Loads GGN data directly from a JSON string.
|
130
|
+
#
|
131
|
+
# This method is useful when you have GGN data as a string (e.g., from a
|
132
|
+
# database, API response, or embedded in your application) rather than a file.
|
133
|
+
#
|
134
|
+
# @param json_string [String] JSON string containing GGN data
|
135
|
+
# @param validate [Boolean] Whether to validate against GGN schema (default: true)
|
136
|
+
#
|
137
|
+
# @return [Piece] A Piece instance containing the parsed GGN data
|
138
|
+
#
|
139
|
+
# @raise [ValidationError] If the JSON is invalid or doesn't conform to GGN schema
|
140
|
+
#
|
141
|
+
# @example Loading GGN data from a string
|
142
|
+
# ggn_json = '{"CHESS:P": {"e2": {"e4": [{"require": {"e3": "empty", "e4": "empty"}, "perform": {"e2": null, "e4": "CHESS:P"}}]}}}'
|
143
|
+
#
|
144
|
+
# begin
|
145
|
+
# piece_data = Sashite::Ggn.load_string(ggn_json)
|
146
|
+
# pawn_source = piece_data.fetch('CHESS:P')
|
147
|
+
# puts "Loaded pawn with move from e2 to e4"
|
148
|
+
# rescue Sashite::Ggn::ValidationError => e
|
149
|
+
# puts "Invalid GGN data: #{e.message}"
|
150
|
+
# end
|
151
|
+
#
|
152
|
+
# @example Loading from API response without validation
|
153
|
+
# api_response = fetch_ggn_from_api()
|
154
|
+
# piece_data = Sashite::Ggn.load_string(api_response.body, validate: false)
|
155
|
+
def load_string(json_string, validate: true)
|
156
|
+
# Parse JSON string with error handling
|
157
|
+
begin
|
158
|
+
data = ::JSON.parse(json_string)
|
159
|
+
rescue ::JSON::ParserError => e
|
160
|
+
raise ValidationError, "Invalid JSON string: #{e.message}"
|
161
|
+
end
|
162
|
+
|
163
|
+
# Validate against GGN schema if requested
|
164
|
+
validate_schema(data, "<string>") if validate
|
165
|
+
|
166
|
+
# Create and return Piece instance
|
167
|
+
Piece.new(data)
|
168
|
+
end
|
169
|
+
|
170
|
+
# Loads GGN data from a Ruby Hash.
|
171
|
+
#
|
172
|
+
# This method is useful when you already have parsed JSON data as a Hash
|
173
|
+
# and want to create a GGN Piece instance with optional validation.
|
174
|
+
#
|
175
|
+
# @param data [Hash] Ruby Hash containing GGN data structure
|
176
|
+
# @param validate [Boolean] Whether to validate against GGN schema (default: true)
|
177
|
+
#
|
178
|
+
# @return [Piece] A Piece instance containing the GGN data
|
179
|
+
#
|
180
|
+
# @raise [ValidationError] If the data doesn't conform to GGN schema (when validation enabled)
|
181
|
+
#
|
182
|
+
# @example Creating from existing Hash data
|
183
|
+
# ggn_data = {
|
184
|
+
# "SHOGI:K" => {
|
185
|
+
# "5i" => {
|
186
|
+
# "4i" => [{ "require" => { "4i" => "empty" }, "perform" => { "5i" => nil, "4i" => "SHOGI:K" } }],
|
187
|
+
# "6i" => [{ "require" => { "6i" => "empty" }, "perform" => { "5i" => nil, "6i" => "SHOGI:K" } }]
|
188
|
+
# }
|
189
|
+
# }
|
190
|
+
# }
|
191
|
+
#
|
192
|
+
# piece_data = Sashite::Ggn.load_hash(ggn_data)
|
193
|
+
# shogi_king = piece_data.fetch('SHOGI:K')
|
194
|
+
def load_hash(data, validate: true)
|
195
|
+
unless data.is_a?(Hash)
|
196
|
+
raise ValidationError, "Expected Hash, got #{data.class}"
|
197
|
+
end
|
198
|
+
|
199
|
+
# Validate against GGN schema if requested
|
200
|
+
validate_schema(data, "<hash>") if validate
|
201
|
+
|
202
|
+
# Create and return Piece instance
|
203
|
+
Piece.new(data)
|
204
|
+
end
|
205
|
+
|
206
|
+
# Validates a data structure against the GGN JSON Schema.
|
207
|
+
#
|
208
|
+
# This method can be used independently to validate GGN data without
|
209
|
+
# creating a Piece instance. Useful for pre-validation or testing.
|
210
|
+
#
|
211
|
+
# @param data [Hash] The data structure to validate
|
212
|
+
# @param context [String] Context information for error messages (default: "<data>")
|
213
|
+
#
|
214
|
+
# @return [true] If validation passes
|
215
|
+
#
|
216
|
+
# @raise [ValidationError] If validation fails with detailed error information
|
217
|
+
#
|
218
|
+
# @example Validating data before processing
|
219
|
+
# begin
|
220
|
+
# Sashite::Ggn.validate!(my_data)
|
221
|
+
# puts "Data is valid GGN format"
|
222
|
+
# rescue Sashite::Ggn::ValidationError => e
|
223
|
+
# puts "Validation failed: #{e.message}"
|
224
|
+
# end
|
225
|
+
def validate!(data, context: "<data>")
|
226
|
+
validate_schema(data, context)
|
227
|
+
true
|
228
|
+
end
|
229
|
+
|
230
|
+
# Checks if a data structure is valid GGN format.
|
231
|
+
#
|
232
|
+
# @param data [Hash] The data structure to validate
|
233
|
+
#
|
234
|
+
# @return [Boolean] true if valid, false otherwise
|
235
|
+
#
|
236
|
+
# @example Checking validity without raising exceptions
|
237
|
+
# if Sashite::Ggn.valid?(my_data)
|
238
|
+
# puts "Data is valid"
|
239
|
+
# else
|
240
|
+
# puts "Data is invalid"
|
241
|
+
# end
|
242
|
+
def valid?(data)
|
243
|
+
schemer = ::JSONSchemer.schema(Schema)
|
244
|
+
schemer.valid?(data)
|
245
|
+
end
|
246
|
+
|
247
|
+
# Returns detailed validation errors for a data structure.
|
248
|
+
#
|
249
|
+
# @param data [Hash] The data structure to validate
|
250
|
+
#
|
251
|
+
# @return [Array<String>] Array of validation error messages (empty if valid)
|
252
|
+
#
|
253
|
+
# @example Getting detailed validation errors
|
254
|
+
# errors = Sashite::Ggn.validation_errors(invalid_data)
|
255
|
+
# if errors.any?
|
256
|
+
# puts "Validation errors found:"
|
257
|
+
# errors.each { |error| puts " - #{error}" }
|
258
|
+
# end
|
259
|
+
def validation_errors(data)
|
260
|
+
schemer = ::JSONSchemer.schema(Schema)
|
261
|
+
schemer.validate(data).map(&:to_s)
|
262
|
+
end
|
263
|
+
|
264
|
+
private
|
265
|
+
|
266
|
+
# Normalizes filepath input to Pathname instance
|
267
|
+
def normalize_filepath(filepath)
|
268
|
+
case filepath
|
269
|
+
when ::Pathname
|
270
|
+
filepath
|
271
|
+
when String
|
272
|
+
::Pathname.new(filepath)
|
273
|
+
else
|
274
|
+
raise ValidationError, "Invalid filepath type: #{filepath.class}. Expected String or Pathname."
|
275
|
+
end
|
276
|
+
end
|
277
|
+
|
278
|
+
# Validates that a file exists and is readable
|
279
|
+
def validate_file_access(file_path)
|
280
|
+
unless file_path.exist?
|
281
|
+
raise ValidationError, "File not found: #{file_path}"
|
282
|
+
end
|
283
|
+
|
284
|
+
unless file_path.readable?
|
285
|
+
raise ValidationError, "File not readable: #{file_path}"
|
286
|
+
end
|
287
|
+
|
288
|
+
unless file_path.file?
|
289
|
+
raise ValidationError, "Path is not a file: #{file_path}"
|
290
|
+
end
|
291
|
+
end
|
292
|
+
|
293
|
+
# Parses JSON file with proper error handling and encoding
|
294
|
+
def parse_json_file(file_path, encoding)
|
295
|
+
# Read file with specified encoding
|
296
|
+
content = file_path.read(encoding: encoding)
|
297
|
+
|
298
|
+
# Parse JSON content
|
299
|
+
::JSON.parse(content)
|
300
|
+
rescue ::JSON::ParserError => e
|
301
|
+
raise ValidationError, "Invalid JSON in file #{file_path}: #{e.message}"
|
302
|
+
rescue ::Encoding::UndefinedConversionError => e
|
303
|
+
raise ValidationError, "Encoding error in file #{file_path}: #{e.message}. Try a different encoding."
|
304
|
+
rescue ::SystemCallError => e
|
305
|
+
raise ValidationError, "Failed to read file #{file_path}: #{e.message}"
|
306
|
+
end
|
307
|
+
|
308
|
+
# Validates data against GGN schema with detailed error reporting
|
309
|
+
def validate_schema(data, context)
|
310
|
+
schemer = ::JSONSchemer.schema(Schema)
|
311
|
+
|
312
|
+
return if schemer.valid?(data)
|
313
|
+
|
314
|
+
# Collect all validation errors for comprehensive feedback
|
315
|
+
errors = schemer.validate(data).map(&:to_s)
|
316
|
+
error_summary = errors.size == 1 ? "1 validation error" : "#{errors.size} validation errors"
|
317
|
+
|
318
|
+
raise ValidationError, "Invalid GGN data in #{context}: #{error_summary}: #{errors.join('; ')}"
|
319
|
+
end
|
8
320
|
end
|
9
321
|
end
|
10
322
|
end
|