kansai_train_info 0.2.1 → 0.2.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b54ebba43519ef36749f5446d252abaeda9f1437143ea8aec1fa1fe97df3c529
4
- data.tar.gz: 659939d8d3be367a78c47a5749e86096a2eb1ab94a56c549e44137ecf2cc11fe
3
+ metadata.gz: 11a5e91f5bf0a274013d50d1f79ffb8dca78683fea2a724602be61f7c6986baa
4
+ data.tar.gz: 557caaccd6c84d19ebc93b17b399cd985d9631cb8f622f328bac6a6ed6418c46
5
5
  SHA512:
6
- metadata.gz: 1ce182a438b00168b73c02e9aa36cf8645be08334f71cd67e9c95211861a020ad63f86d83b47ad63996fb83e525897f6687fda3154f5f2c6cf9fd7a879bb71ca
7
- data.tar.gz: f4cde648707106d080357156c979647c8a658aba7fd65540556f2750affd7f83353c50b0935deb047d2cd4c1c57343b4e069b3dcb7a1ac0de52a09049f2eeb65
6
+ metadata.gz: 56f319c4be59afc3fc01baa6c7f67ecd6b5b5179f8a489864b38b2eecce6ff598f3a8d7417f61ad0649d74bb5b2c4aa84cc7234f0be3a4041cc9d43d2d6141fd
7
+ data.tar.gz: 862d0ddc6c454c6da87579e7f6f6bd6ea17af1f2601d7e2593b6732f0c3e9f799f3adbee187e5873969e7ae0b8f93eda7e4ebc6f0030f4056744698ff409376a
data/LICENSE.txt CHANGED
@@ -1,21 +1,18 @@
1
- The MIT License (MIT)
1
+ kansai_train_info is licensed under the GPLv3 license for all open source applications.
2
2
 
3
- Copyright (c) 2020 okamotchan
3
+ Please do not use this project for commercial use, it is not intended to be used for commercial use.
4
4
 
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
5
+ Copyright (C) 2020 okamotchan
11
6
 
12
- The above copyright notice and this permission notice shall be included in
13
- all copies or substantial portions of the Software.
7
+ This program is free software: you can redistribute it and/or modify
8
+ it under the terms of the GNU General Public License as published by
9
+ the Free Software Foundation, either version 3 of the License, or
10
+ (at your option) any later version.
14
11
 
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
- THE SOFTWARE.
12
+ This program is distributed in the hope that it will be useful,
13
+ but WITHOUT ANY WARRANTY; without even the implied warranty of
14
+ MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15
+ GNU General Public License for more details.
16
+
17
+ You should have received a copy of the GNU General Public License
18
+ along with this program. If not, see <https://www.gnu.org/licenses/>.
data/README.md CHANGED
@@ -1,54 +1,172 @@
1
1
  # kansai_train_info
2
2
 
3
- obtain train operation status in the Kansai region of Japan
3
+ [![CircleCI](https://dl.circleci.com/status-badge/img/gh/o8n/kansai_train_info/tree/master.svg?style=svg)](https://dl.circleci.com/status-badge/redirect/gh/o8n/kansai_train_info/tree/master)
4
+ [![Gem Version](https://badge.fury.io/rb/kansai_train_info.svg)](https://badge.fury.io/rb/kansai_train_info)
5
+ [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0)
6
+
7
+ 関西地方の鉄道運行情報を取得するRuby Gemです。
8
+
9
+ [English](README.en.md)
10
+
11
+ ## Features
12
+
13
+ - 🚃 関西主要路線の運行情報をリアルタイム取得
14
+ - 🔄 自動リトライ機能(指数バックオフ付き)
15
+ - ⚡ 高速なHTTP通信とHTML解析
16
+ - 🛡️ 包括的なエラーハンドリング
17
+ - 📝 完全な型定義(RBS)
18
+ - 🎯 90%以上のテストカバレッジ
19
+
20
+ ## Installation
21
+
22
+ Gemをインストール:
23
+
24
+ ```bash
25
+ gem install kansai_train_info
26
+ ```
27
+
28
+ または、Gemfileに追加:
29
+
30
+ ```ruby
31
+ gem 'kansai_train_info', '~> 0.2.0'
32
+ ```
4
33
 
5
34
  ## Usage
6
35
 
36
+ ### Ruby
37
+
38
+ ```ruby
39
+ require 'kansai_train_info'
40
+
41
+ # 単一路線の情報を取得
42
+ KansaiTrainInfo.get(['大阪環状線'])
43
+ # => 大阪環状線は平常運転です
44
+
45
+ # 複数路線の情報を取得
46
+ KansaiTrainInfo.get(['大阪環状線', '御堂筋線'])
47
+ # => "大阪環状線は列車遅延があります。10分程度の遅れが発生しています。"
48
+
49
+ # 詳細URLを含める
50
+ KansaiTrainInfo.get(['大阪環状線'], url: true)
51
+ # => "大阪環状線は列車遅延があります。https://transit.yahoo.co.jp/traininfo/detail/263/0/"
52
+
53
+ # 利用可能な路線を表示
54
+ KansaiTrainInfo.help
55
+ ```
56
+
57
+ ### CLI
58
+
59
+ ```bash
60
+ # 単一路線
61
+ kansai_train_info get 大阪環状線
62
+
63
+ # 複数路線
64
+ kansai_train_info get 大阪環状線 御堂筋線
65
+
66
+ # URLを含める
67
+ kansai_train_info get 大阪環状線 --url
68
+
69
+ # ヘルプ
70
+ kansai_train_info help
71
+ ```
72
+
73
+ ## Supported Lines
74
+
75
+ - 大阪環状線
76
+ - 近鉄京都線
77
+ - 阪急京都線
78
+ - 御堂筋線
79
+ - 烏丸線
80
+ - 東西線
81
+
82
+ ## Configuration
83
+
84
+ カスタム設定が可能です:
85
+
86
+ ```ruby
87
+ KansaiTrainInfo.configure do |config|
88
+ config.timeout = 30 # タイムアウト時間(秒)
89
+ config.max_retries = 5 # 最大リトライ回数
90
+ config.retry_delay = 2 # リトライ間隔の基準時間(秒)
91
+ config.user_agent = 'MyApp/1.0'
92
+ end
93
+ ```
94
+
95
+ 詳細は[Configuration Guide](docs/CONFIGURATION.md)を参照してください。
96
+
97
+ ## Requirements
98
+
99
+ - Ruby >= 3.0.0
7
100
 
8
- `gem install kansai_train_info`
101
+ ## Development
9
102
 
10
- or
103
+ ### Testing
11
104
 
12
- gemfile: `gem 'kansai_train_info', '~> 0.2.0'`
105
+ Run tests with coverage report:
13
106
 
107
+ ```sh
108
+ # Run all tests
109
+ bundle exec rspec
110
+
111
+ # Run specific test file
112
+ bundle exec rspec spec/kansai_train_info/client_spec.rb
14
113
 
15
- ``` sh
16
- irb(main):001:0> require 'kansai_train_info'
17
- => true
18
- irb(main):002:0> KansaiTrainInfo.get(['大阪環状線'])
19
- 大阪環状線は平常運転です
114
+ # View coverage report
115
+ open coverage/index.html
20
116
  ```
21
117
 
22
- now support: `大阪環状線, 近鉄京都線, 阪急京都線, 御堂筋線, 烏丸線, 東西線`
118
+ The project maintains a minimum test coverage of 90%.
23
119
 
24
- ## requirements
120
+ ### Type Checking
25
121
 
26
- Ruby >= 3.0.0
122
+ This gem uses RBS for type definitions and Steep for type checking.
27
123
 
124
+ ```sh
125
+ # Run type checking
126
+ bundle exec steep check
28
127
 
29
- <details><summary>Trouble Shoot</summary>
128
+ # Or use rake task
129
+ bundle exec rake steep
130
+ ```
131
+
132
+
133
+ ### Linting
30
134
 
31
- ### can't read gem
135
+ Rubocopでコードスタイルをチェック:
32
136
 
33
137
  ```sh
34
- irb(main):001:0> require 'kansai_train_info'
35
- Traceback (most recent call last):
36
- 6: from /Users/name/.rbenv/versions/2.7.1/bin/irb:23:in `<main>'
37
- 5: from /Users/name/.rbenv/versions/2.7.1/bin/irb:23:in `load'
38
- 4: from /Users/name/.rbenv/versions/2.7.1/lib/ruby/gems/2.7.0/gems/irb-1.2.3/exe/irb:11:in `<top (required)>'
39
- 3: from (irb):1
40
- 2: from /Users/name/.rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require'
41
- 1: from /Users/name/.rbenv/versions/2.7.1/lib/ruby/2.7.0/rubygems/core_ext/kernel_require.rb:92:in `require'
42
- LoadError (cannot load such file -- KansaiTrainInfo)
138
+ bundle exec rubocop
43
139
  ```
44
140
 
45
- then excute
141
+ ## Documentation
142
+
143
+ - [API Documentation](docs/API.md) - 詳細なAPIリファレンス ([English](docs/API.en.md))
144
+ - [Configuration Guide](docs/CONFIGURATION.md) - 設定オプションの詳細 ([English](docs/CONFIGURATION.en.md))
145
+ - [Contributing Guide](CONTRIBUTING.md) - 貢献方法 ([English](CONTRIBUTING.en.md))
146
+ - [Changelog](CHANGELOG.md) - 変更履歴
147
+
148
+ ## Error Handling
149
+
150
+ このgemは以下のカスタムエラーを提供します:
46
151
 
47
- ```txt
48
- irb(main):002:0> $:
49
- irb(main):003:0> $: << 'lib'
50
- irb(main):012:0> require 'KansaiTrainInfo'
51
- => true
152
+ - `KansaiTrainInfo::NetworkError` - ネットワーク関連のエラー
153
+ - `KansaiTrainInfo::TimeoutError` - タイムアウトエラー
154
+ - `KansaiTrainInfo::ParseError` - HTML解析エラー
155
+ - `KansaiTrainInfo::InvalidRouteError` - 無効な路線名エラー
156
+
157
+ ```ruby
158
+ begin
159
+ KansaiTrainInfo.get(['存在しない路線'])
160
+ rescue KansaiTrainInfo::InvalidRouteError => e
161
+ puts "エラー: #{e.message}"
162
+ end
52
163
  ```
53
164
 
54
- </details>
165
+ ## License
166
+
167
+ kansai_train_info is licensed under the GPLv3 license for all open source applications.
168
+
169
+ Please do not use this project for commercial use, it is not intended to be used for commercial use.
170
+
171
+ See [LICENSE.txt](LICENSE.txt) for details.
172
+
@@ -1,15 +1,12 @@
1
1
  module KansaiTrainInfo
2
2
  class CLI < Thor
3
- desc "KansaiTrainInfo *routes", "Get"
3
+ desc 'KansaiTrainInfo *routes', 'Get'
4
4
  def get(*routes)
5
- texts = KansaiTrainInfo.get(routes)
6
- texts.each do |text|
7
- print text
8
- print "\n"
9
- end
5
+ result = KansaiTrainInfo.get(routes)
6
+ puts result if result
10
7
  end
11
8
 
12
- desc "KansaiTrainInfo help", "Help"
9
+ desc 'KansaiTrainInfo help', 'Help'
13
10
  def help
14
11
  KansaiTrainInfo.help
15
12
  end
@@ -1,9 +1,20 @@
1
- # flozen_string_literal: true
2
- require 'open-uri'
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'timeout'
3
5
  require 'nokogiri'
6
+ require 'kansai_train_info/errors'
7
+ require 'kansai_train_info/configuration'
8
+ require 'kansai_train_info/http_client'
9
+ require 'kansai_train_info/parser'
10
+ require 'kansai_train_info/route'
11
+ require 'kansai_train_info/status_formatter'
4
12
 
13
+ # rubocop:disable Metrics/ModuleLength
5
14
  module KansaiTrainInfo
15
+ # rubocop:disable Metrics/ClassLength
6
16
  class << self
17
+ # Legacy constants for backward compatibility
7
18
  LINES = {
8
19
  大阪環状線: [4, 2, 263],
9
20
  近鉄京都線: [6, 5, 288],
@@ -13,72 +24,174 @@ module KansaiTrainInfo
13
24
  東西線: [34, 3, 319]
14
25
  }.freeze
15
26
 
16
- # rubocop:disable Metrics/AbcSize, Layout/LineLength
27
+ DEFAULT_TIMEOUT = 10
28
+ MAX_RETRIES = 3
29
+
30
+ # 指定された路線の運行情報を取得する
31
+ #
32
+ # @param route_array [Array<String>] 取得したい路線名の配列
33
+ # @param url [Boolean] 詳細URLを含めるかどうか(デフォルト: false)
34
+ # @return [String, nil] 運行情報のメッセージ。正常運転の場合はnil
35
+ # @raise [InvalidRouteError] 無効な路線名が指定された場合
36
+ #
37
+ # @example 単一路線の情報を取得
38
+ # KansaiTrainInfo.get(['大阪環状線'])
39
+ #
40
+ # @example 複数路線でURLを含める
41
+ # KansaiTrainInfo.get(['大阪環状線', '御堂筋線'], url: true)
42
+ #
43
+ # rubocop:disable Metrics/PerceivedComplexity
17
44
  def get(route_array, url: false)
45
+ if route_array.empty?
46
+ puts '利用可能な路線を入力してください'
47
+ return nil
48
+ end
49
+
18
50
  messages = []
19
51
 
20
- route_array.each do |route|
21
- line = LINES[route.to_sym]
22
- raise KeyError, "Invalid route: #{route}" unless line
52
+ route_array.each do |route_name|
53
+ route = route_registry.find(route_name)
54
+ raise InvalidRouteError, "Invalid route: #{route_name}" unless route
23
55
 
24
- status_xpath = "//*[@id='mdAreaMajorLine']/div[#{LINES[route.to_sym][0]}]/table/tr[#{LINES[route.to_sym][1]}]/td[2]"
25
- detail_url = "https://transit.yahoo.co.jp/traininfo/detail/#{LINES[route.to_sym][2]}/0/"
26
- state = kansai_doc.xpath(status_xpath).first&.text
27
- messages << message(route, state, url, detail_url)
28
- end
29
- if messages.empty?
30
- puts '利用可能な路線を入力してください'
31
- else
32
- messages.join(', ')
56
+ begin
57
+ status = fetch_route_status(route)
58
+ description = status && status != '平常運転' ? fetch_description(route.detail_url) : nil
59
+
60
+ formatter = StatusFormatter.new(route_name, status)
61
+ formatted_message = formatter.format(
62
+ include_url: url,
63
+ detail_url: route.detail_url,
64
+ description: description
65
+ )
66
+
67
+ messages << formatted_message if formatted_message
68
+ rescue NetworkError => e
69
+ messages << "#{route_name}: ネットワークエラー - #{e.message}"
70
+ rescue ParseError => e
71
+ messages << "#{route_name}: データ解析エラー - #{e.message}"
72
+ end
33
73
  end
74
+
75
+ return nil if messages.empty?
76
+
77
+ messages.compact.join(', ')
34
78
  end
35
- # rubocop:enable Metrics/AbcSize, Layout/LineLength
79
+ # rubocop:enable Metrics/PerceivedComplexity
36
80
 
37
- def kansai_doc
38
- charset = nil
39
- url = 'https://transit.yahoo.co.jp/traininfo/area/6/'
81
+ # 利用可能な路線を表示する
82
+ #
83
+ # @return [void]
84
+ #
85
+ # @example
86
+ # KansaiTrainInfo.help
87
+ # # => 利用可能な路線:
88
+ # # => 大阪環状線、近鉄京都線、阪急京都線, 御堂筋線, 烏丸線, 東西線
89
+ def help
90
+ help_message = "利用可能な路線:\n#{route_registry.names.join('、')}"
91
+ puts help_message
92
+ end
40
93
 
41
- html = URI.open(url) do |f|
42
- charset = f.charset
43
- f.read
44
- end
94
+ private
95
+
96
+ def route_registry
97
+ @route_registry ||= RouteRegistry.new
98
+ end
99
+
100
+ def http_client
101
+ @http_client ||= HttpClient.new
102
+ end
103
+
104
+ def fetch_route_status(route)
105
+ url = "#{KansaiTrainInfo.configuration.base_url}/traininfo/area/6/"
106
+ html = http_client.get(url)
107
+ parser = Parser.new(html)
108
+ parser.extract_status(route.status_xpath)
109
+ end
45
110
 
46
- Nokogiri::HTML.parse(html, nil, charset)
111
+ def fetch_description(detail_url)
112
+ html = http_client.get(detail_url)
113
+ parser = Parser.new(html)
114
+ parser.extract_detail('//*[@id="mdServiceStatus"]/dl/dd/p')
115
+ rescue NetworkError => e
116
+ "詳細情報取得エラー: #{e.message}"
117
+ rescue StandardError => e
118
+ "詳細情報解析エラー: #{e.message}"
47
119
  end
48
120
 
121
+ # Legacy method for backward compatibility
49
122
  def description(detail_url)
50
- charset = nil
51
- detail_html = URI.open(detail_url) do |f|
52
- charset = f.charset
53
- f.read
54
- end
55
- detail_doc = Nokogiri::HTML.parse(detail_html, nil, charset)
56
- detail_doc.xpath('//*[@id="mdServiceStatus"]/dl/dd/p').first.text
123
+ html = http_client.get(detail_url)
124
+ parser = Parser.new(html)
125
+ result = parser.extract_detail('//*[@id="mdServiceStatus"]/dl/dd/p')
126
+ result || '詳細情報を取得できませんでした'
127
+ rescue StandardError
128
+ '詳細情報を取得できませんでした'
57
129
  end
58
130
 
59
- # rubocop:disable Metrics/CyclomaticComplexity
131
+ # Legacy method for backward compatibility
60
132
  def message(route, state, url, detail_url)
61
133
  return "#{route}は運行情報がありません" if state.nil?
62
134
 
63
- state&.slice!('[○]')
64
- state&.slice!('[!]')
65
- puts "#{route}は#{state}です" if state == '平常運転'
66
- message = case state
135
+ # Remove status indicators without modifying the original string
136
+ clean_state = state.gsub('[○]', '').gsub('[!]', '')
137
+ puts "#{route}は#{clean_state}です" if clean_state == '平常運転'
138
+ message = case clean_state
67
139
  when '運転状況'
68
- "#{route}は#{state}に変更があります。"
140
+ "#{route}は#{clean_state}に変更があります。"
69
141
  when '列車遅延'
70
- "#{route}は#{state}があります。"
142
+ "#{route}は#{clean_state}があります。"
71
143
  when '運転見合わせ'
72
- "#{route}は#{state}しています。"
144
+ "#{route}は#{clean_state}しています。"
73
145
  end
74
- show_message = "#{message} #{description(detail_url)}"
146
+ return "#{route}は#{clean_state}です" if message.nil?
147
+
148
+ desc = description(detail_url)
149
+ show_message = "#{message} #{desc}"
75
150
  url ? show_message + detail_url : show_message
76
151
  end
77
- # rubocop:enable Metrics/CyclomaticComplexity
78
152
 
79
- def help
80
- help_message = "利用可能な路線:\n大阪環状線、近鉄京都線、阪急京都線, 御堂筋線, 烏丸線, 東西線"
81
- puts help_message
153
+ # Legacy method for backward compatibility
154
+ # rubocop:disable Metrics/AbcSize
155
+ def fetch_url(url, retries = 0)
156
+ uri = URI.parse(url)
157
+
158
+ Timeout.timeout(KansaiTrainInfo.configuration.timeout) do
159
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
160
+ request = Net::HTTP::Get.new(uri)
161
+ request['User-Agent'] = KansaiTrainInfo.configuration.user_agent
162
+ response = http.request(request)
163
+
164
+ case response
165
+ when Net::HTTPSuccess
166
+ response.body.dup.force_encoding('UTF-8')
167
+ else
168
+ raise NetworkError, "HTTP Error: #{response.code} #{response.message}"
169
+ end
170
+ end
171
+ end
172
+ rescue Timeout::Error
173
+ raise TimeoutError, "Request timeout after #{KansaiTrainInfo.configuration.timeout} seconds"
174
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
175
+ unless retries < KansaiTrainInfo.configuration.max_retries
176
+ raise NetworkError, "Connection failed after #{KansaiTrainInfo.configuration.max_retries} retries: #{e.message}"
177
+ end
178
+
179
+ sleep(KansaiTrainInfo.configuration.retry_delays[retries])
180
+ fetch_url(url, retries + 1)
181
+ rescue StandardError => e
182
+ raise NetworkError, "Network error: #{e.message}"
183
+ end
184
+ # rubocop:enable Metrics/AbcSize
185
+
186
+ # Legacy method for backward compatibility
187
+ def kansai_doc
188
+ url = "#{KansaiTrainInfo.configuration.base_url}/traininfo/area/6/"
189
+ html = fetch_url(url)
190
+ Nokogiri::HTML.parse(html, nil, 'utf-8')
191
+ rescue StandardError => e
192
+ raise ParseError, "HTMLの解析に失敗しました: #{e.message}"
82
193
  end
83
194
  end
195
+ # rubocop:enable Metrics/ClassLength
84
196
  end
197
+ # rubocop:enable Metrics/ModuleLength
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KansaiTrainInfo
4
+ class Configuration
5
+ attr_accessor :user_agent, :timeout, :max_retries, :retry_delay, :base_url
6
+
7
+ def initialize
8
+ @user_agent = "kansai_train_info/#{VERSION}"
9
+ @timeout = 10
10
+ @max_retries = 3
11
+ @retry_delay = 1
12
+ @base_url = 'https://transit.yahoo.co.jp'
13
+ end
14
+
15
+ def retry_delays
16
+ (0...@max_retries).map { |i| @retry_delay * (2**i) }
17
+ end
18
+ end
19
+
20
+ class << self
21
+ def configuration
22
+ @configuration ||= Configuration.new
23
+ end
24
+
25
+ def configure
26
+ yield(configuration)
27
+ end
28
+
29
+ def reset_configuration!
30
+ @configuration = Configuration.new
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KansaiTrainInfo
4
+ class Error < StandardError; end
5
+
6
+ class NetworkError < Error; end
7
+
8
+ class TimeoutError < NetworkError; end
9
+
10
+ class ParseError < Error; end
11
+
12
+ class InvalidRouteError < Error; end
13
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+ require 'timeout'
5
+
6
+ module KansaiTrainInfo
7
+ class HttpClient
8
+ def initialize(config = KansaiTrainInfo.configuration)
9
+ @config = config
10
+ end
11
+
12
+ # rubocop:disable Metrics/AbcSize
13
+ def get(url, retries: 0)
14
+ uri = URI.parse(url)
15
+
16
+ Timeout.timeout(@config.timeout) do
17
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
18
+ request = Net::HTTP::Get.new(uri)
19
+ request['User-Agent'] = @config.user_agent
20
+ response = http.request(request)
21
+
22
+ case response
23
+ when Net::HTTPSuccess
24
+ response.body.dup.force_encoding('UTF-8')
25
+ else
26
+ raise NetworkError, "HTTP Error: #{response.code} #{response.message}"
27
+ end
28
+ end
29
+ end
30
+ rescue Timeout::Error
31
+ raise TimeoutError, "Request timeout after #{@config.timeout} seconds"
32
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH => e
33
+ unless retries < @config.max_retries
34
+ raise NetworkError, "Connection failed after #{@config.max_retries} retries: #{e.message}"
35
+ end
36
+
37
+ sleep(@config.retry_delays[retries])
38
+ get(url, retries: retries + 1)
39
+ rescue StandardError => e
40
+ raise NetworkError, "Network error: #{e.message}"
41
+ end
42
+ # rubocop:enable Metrics/AbcSize
43
+ end
44
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'nokogiri'
4
+
5
+ module KansaiTrainInfo
6
+ class Parser
7
+ attr_reader :document
8
+
9
+ def initialize(html)
10
+ @document = Nokogiri::HTML.parse(html, nil, 'utf-8')
11
+ rescue StandardError => e
12
+ raise ParseError, "HTMLの解析に失敗しました: #{e.message}"
13
+ end
14
+
15
+ def extract_status(xpath)
16
+ @document.xpath(xpath).first&.text
17
+ end
18
+
19
+ def extract_detail(xpath)
20
+ element = @document.xpath(xpath).first
21
+ element&.text || '詳細情報を取得できませんでした'
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KansaiTrainInfo
4
+ class Route
5
+ attr_reader :name, :area_index, :row_index, :detail_id
6
+
7
+ def initialize(name:, area_index:, row_index:, detail_id:)
8
+ @name = name
9
+ @area_index = area_index
10
+ @row_index = row_index
11
+ @detail_id = detail_id
12
+ end
13
+
14
+ def status_xpath
15
+ "//*[@id='mdAreaMajorLine']/div[#{area_index}]/table/tr[#{row_index}]/td[2]"
16
+ end
17
+
18
+ def detail_url
19
+ "#{KansaiTrainInfo.configuration.base_url}/traininfo/detail/#{detail_id}/0/"
20
+ end
21
+
22
+ def to_sym
23
+ name.to_sym
24
+ end
25
+ end
26
+
27
+ class RouteRegistry
28
+ def initialize
29
+ @routes = {}
30
+ register_default_routes
31
+ end
32
+
33
+ def register(name, area_index:, row_index:, detail_id:)
34
+ route = Route.new(
35
+ name: name,
36
+ area_index: area_index,
37
+ row_index: row_index,
38
+ detail_id: detail_id
39
+ )
40
+ @routes[name.to_sym] = route
41
+ end
42
+
43
+ def find(name)
44
+ @routes[name.to_sym]
45
+ end
46
+
47
+ def all
48
+ @routes.values
49
+ end
50
+
51
+ def names
52
+ @routes.keys.map(&:to_s)
53
+ end
54
+
55
+ private
56
+
57
+ def register_default_routes
58
+ register('大阪環状線', area_index: 4, row_index: 2, detail_id: 263)
59
+ register('近鉄京都線', area_index: 6, row_index: 5, detail_id: 288)
60
+ register('阪急京都線', area_index: 8, row_index: 2, detail_id: 306)
61
+ register('御堂筋線', area_index: 10, row_index: 3, detail_id: 321)
62
+ register('烏丸線', area_index: 34, row_index: 2, detail_id: 318)
63
+ register('東西線', area_index: 34, row_index: 3, detail_id: 319)
64
+ end
65
+ end
66
+
67
+ class << self
68
+ def route_registry
69
+ @route_registry ||= RouteRegistry.new
70
+ end
71
+ end
72
+ end
@@ -1,18 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'nokogiri'
4
- require 'open-uri'
4
+ require 'net/http'
5
+ require 'timeout'
5
6
 
6
7
  url = 'https://transit.yahoo.co.jp/traininfo/area/6/'
7
8
 
8
- charset = nil
9
+ uri = URI.parse(url)
10
+ html = Timeout.timeout(10) do
11
+ Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == 'https') do |http|
12
+ request = Net::HTTP::Get.new(uri)
13
+ response = http.request(request)
9
14
 
10
- html = open(url) do |f|
11
- charset = f.charset
12
- f.read
15
+ case response
16
+ when Net::HTTPSuccess
17
+ # Create a mutable copy before force_encoding
18
+ response.body.dup.force_encoding('UTF-8')
19
+ else
20
+ raise "HTTP Error: #{response.code} #{response.message}"
21
+ end
22
+ end
13
23
  end
14
24
 
15
- doc = Nokogiri::HTML.parse(html, nil, charset)
25
+ doc = Nokogiri::HTML.parse(html, nil, 'utf-8')
16
26
 
17
27
  status_xpath = "//*[@id='mdAreaMajorLine']/div[4]/table/tr[3]/td[1]"
18
28
 
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KansaiTrainInfo
4
+ class StatusFormatter
5
+ STATUS_INDICATORS = {
6
+ normal: '[○]',
7
+ alert: '[!]'
8
+ }.freeze
9
+
10
+ STATUS_MESSAGES = {
11
+ '運転状況' => ->(route) { "#{route}は運転状況に変更があります。" },
12
+ '列車遅延' => ->(route) { "#{route}は列車遅延があります。" },
13
+ '運転見合わせ' => ->(route) { "#{route}は運転見合わせしています。" }
14
+ }.freeze
15
+
16
+ def initialize(route_name, raw_status)
17
+ @route_name = route_name
18
+ @raw_status = raw_status
19
+ end
20
+
21
+ def format(include_url: false, detail_url: nil, description: nil)
22
+ return "#{@route_name}は運行情報がありません" if @raw_status.nil?
23
+
24
+ status = clean_status(@raw_status)
25
+
26
+ if status == '平常運転'
27
+ puts "#{@route_name}は#{status}です"
28
+ return nil
29
+ end
30
+
31
+ message = build_message(status, description)
32
+ include_url && detail_url ? "#{message}#{detail_url}" : message
33
+ end
34
+
35
+ private
36
+
37
+ def clean_status(raw_status)
38
+ STATUS_INDICATORS.values.reduce(raw_status) do |status, indicator|
39
+ status.gsub(indicator, '')
40
+ end
41
+ end
42
+
43
+ def build_message(status, description)
44
+ base_message = STATUS_MESSAGES[status]&.call(@route_name) || "#{@route_name}は#{status}です"
45
+ description ? "#{base_message} #{description}" : base_message
46
+ end
47
+ end
48
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module KansaiTrainInfo
4
- VERSION = '0.2.1'
4
+ VERSION = '0.2.2'
5
5
  end
@@ -1,7 +1,13 @@
1
- require "thor"
2
- require "kansai_train_info/version"
3
- require "kansai_train_info/cli"
4
- require "kansai_train_info/client"
1
+ require 'thor'
2
+ require 'kansai_train_info/version'
3
+ require 'kansai_train_info/errors'
4
+ require 'kansai_train_info/configuration'
5
+ require 'kansai_train_info/http_client'
6
+ require 'kansai_train_info/parser'
7
+ require 'kansai_train_info/route'
8
+ require 'kansai_train_info/status_formatter'
9
+ require 'kansai_train_info/client'
10
+ require 'kansai_train_info/cli'
5
11
 
6
12
  module KansaiTrainInfo
7
13
  end
metadata CHANGED
@@ -1,85 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kansai_train_info
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.2.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - o8n
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-07-15 00:00:00.000000000 Z
11
+ date: 2025-07-29 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: nokogiri
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - ">="
17
+ - - "~>"
18
18
  - !ruby/object:Gem::Version
19
- version: '0'
19
+ version: '1.15'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - ">="
24
+ - - "~>"
25
25
  - !ruby/object:Gem::Version
26
- version: '0'
26
+ version: '1.15'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: thor
29
- requirement: !ruby/object:Gem::Requirement
30
- requirements:
31
- - - ">="
32
- - !ruby/object:Gem::Version
33
- version: '0'
34
- type: :runtime
35
- prerelease: false
36
- version_requirements: !ruby/object:Gem::Requirement
37
- requirements:
38
- - - ">="
39
- - !ruby/object:Gem::Version
40
- version: '0'
41
- - !ruby/object:Gem::Dependency
42
- name: bundler
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - ">="
46
- - !ruby/object:Gem::Version
47
- version: '0'
48
- type: :development
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - ">="
53
- - !ruby/object:Gem::Version
54
- version: '0'
55
- - !ruby/object:Gem::Dependency
56
- name: rake
57
29
  requirement: !ruby/object:Gem::Requirement
58
30
  requirements:
59
31
  - - "~>"
60
32
  - !ruby/object:Gem::Version
61
- version: '12.3'
62
- type: :development
33
+ version: '1.0'
34
+ type: :runtime
63
35
  prerelease: false
64
36
  version_requirements: !ruby/object:Gem::Requirement
65
37
  requirements:
66
38
  - - "~>"
67
39
  - !ruby/object:Gem::Version
68
- version: '12.3'
69
- - !ruby/object:Gem::Dependency
70
- name: rspec
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - ">="
74
- - !ruby/object:Gem::Version
75
- version: '0'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - ">="
81
- - !ruby/object:Gem::Version
82
- version: '0'
40
+ version: '1.0'
83
41
  description: you can check train info in kansai area
84
42
  email:
85
43
  - m.okamotchan@gmail.com
@@ -92,12 +50,19 @@ files:
92
50
  - lib/kansai_train_info.rb
93
51
  - lib/kansai_train_info/cli.rb
94
52
  - lib/kansai_train_info/client.rb
53
+ - lib/kansai_train_info/configuration.rb
54
+ - lib/kansai_train_info/errors.rb
55
+ - lib/kansai_train_info/http_client.rb
56
+ - lib/kansai_train_info/parser.rb
57
+ - lib/kansai_train_info/route.rb
95
58
  - lib/kansai_train_info/script.rb
59
+ - lib/kansai_train_info/status_formatter.rb
96
60
  - lib/kansai_train_info/version.rb
97
61
  homepage:
98
62
  licenses:
99
- - MIT
100
- metadata: {}
63
+ - GPL-3.0
64
+ metadata:
65
+ rubygems_mfa_required: 'true'
101
66
  post_install_message:
102
67
  rdoc_options: []
103
68
  require_paths:
@@ -106,7 +71,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
106
71
  requirements:
107
72
  - - ">="
108
73
  - !ruby/object:Gem::Version
109
- version: 2.3.0
74
+ version: 3.0.0
110
75
  required_rubygems_version: !ruby/object:Gem::Requirement
111
76
  requirements:
112
77
  - - ">="