rail_feeds 0.0.1
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 +7 -0
- data/.gitignore +23 -0
- data/.rspec +3 -0
- data/.rubocop.yml +31 -0
- data/.travis.yml +26 -0
- data/CHANGELOG.md +3 -0
- data/Gemfile +6 -0
- data/Guardfile +25 -0
- data/LICENSE.md +32 -0
- data/README.md +77 -0
- data/Rakefile +3 -0
- data/doc/guides/Logging.md +13 -0
- data/doc/guides/Network Rail/CORPUS.md +34 -0
- data/doc/guides/Network Rail/SMART.md +39 -0
- data/doc/guides/Network Rail/Schedule.md +138 -0
- data/file +0 -0
- data/lib/rail_feeds/credentials.rb +45 -0
- data/lib/rail_feeds/logging.rb +51 -0
- data/lib/rail_feeds/network_rail/corpus.rb +77 -0
- data/lib/rail_feeds/network_rail/credentials.rb +22 -0
- data/lib/rail_feeds/network_rail/http_client.rb +57 -0
- data/lib/rail_feeds/network_rail/schedule/association.rb +208 -0
- data/lib/rail_feeds/network_rail/schedule/data.rb +215 -0
- data/lib/rail_feeds/network_rail/schedule/days.rb +95 -0
- data/lib/rail_feeds/network_rail/schedule/fetcher.rb +193 -0
- data/lib/rail_feeds/network_rail/schedule/header/cif.rb +102 -0
- data/lib/rail_feeds/network_rail/schedule/header/json.rb +79 -0
- data/lib/rail_feeds/network_rail/schedule/header.rb +22 -0
- data/lib/rail_feeds/network_rail/schedule/parser/cif.rb +141 -0
- data/lib/rail_feeds/network_rail/schedule/parser/json.rb +87 -0
- data/lib/rail_feeds/network_rail/schedule/parser.rb +108 -0
- data/lib/rail_feeds/network_rail/schedule/stp_indicator.rb +72 -0
- data/lib/rail_feeds/network_rail/schedule/tiploc.rb +100 -0
- data/lib/rail_feeds/network_rail/schedule/train_schedule/change_en_route.rb +158 -0
- data/lib/rail_feeds/network_rail/schedule/train_schedule/location/intermediate.rb +119 -0
- data/lib/rail_feeds/network_rail/schedule/train_schedule/location/origin.rb +91 -0
- data/lib/rail_feeds/network_rail/schedule/train_schedule/location/terminating.rb +72 -0
- data/lib/rail_feeds/network_rail/schedule/train_schedule/location.rb +76 -0
- data/lib/rail_feeds/network_rail/schedule/train_schedule.rb +392 -0
- data/lib/rail_feeds/network_rail/schedule.rb +33 -0
- data/lib/rail_feeds/network_rail/smart.rb +186 -0
- data/lib/rail_feeds/network_rail/stomp_client.rb +77 -0
- data/lib/rail_feeds/network_rail.rb +16 -0
- data/lib/rail_feeds/version.rb +14 -0
- data/lib/rail_feeds.rb +10 -0
- data/rail_feeds.gemspec +32 -0
- data/spec/fixtures/network_rail/schedule/data/full.yaml +60 -0
- data/spec/fixtures/network_rail/schedule/data/starting.yaml +131 -0
- data/spec/fixtures/network_rail/schedule/data/update-gap.yaml +10 -0
- data/spec/fixtures/network_rail/schedule/data/update-next.yaml +13 -0
- data/spec/fixtures/network_rail/schedule/data/update-old.yaml +10 -0
- data/spec/fixtures/network_rail/schedule/data/update.yaml +112 -0
- data/spec/fixtures/network_rail/schedule/parser/train_create.json +1 -0
- data/spec/fixtures/network_rail/schedule/parser/train_delete.json +1 -0
- data/spec/fixtures/network_rail/schedule/train_schedule/json-data.yaml +67 -0
- data/spec/rail_feeds/credentials_spec.rb +46 -0
- data/spec/rail_feeds/logging_spec.rb +81 -0
- data/spec/rail_feeds/network_rail/corpus_spec.rb +92 -0
- data/spec/rail_feeds/network_rail/credentials_spec.rb +22 -0
- data/spec/rail_feeds/network_rail/http_client_spec.rb +88 -0
- data/spec/rail_feeds/network_rail/schedule/association_spec.rb +205 -0
- data/spec/rail_feeds/network_rail/schedule/data_spec.rb +219 -0
- data/spec/rail_feeds/network_rail/schedule/days_shared.rb +99 -0
- data/spec/rail_feeds/network_rail/schedule/days_spec.rb +4 -0
- data/spec/rail_feeds/network_rail/schedule/fetcher_spec.rb +228 -0
- data/spec/rail_feeds/network_rail/schedule/header/cif_spec.rb +72 -0
- data/spec/rail_feeds/network_rail/schedule/header/json_spec.rb +51 -0
- data/spec/rail_feeds/network_rail/schedule/header_spec.rb +19 -0
- data/spec/rail_feeds/network_rail/schedule/parser/cif_spec.rb +197 -0
- data/spec/rail_feeds/network_rail/schedule/parser/json_spec.rb +172 -0
- data/spec/rail_feeds/network_rail/schedule/parser_spec.rb +34 -0
- data/spec/rail_feeds/network_rail/schedule/stp_indicator_shared.rb +49 -0
- data/spec/rail_feeds/network_rail/schedule/stp_indicator_spec.rb +4 -0
- data/spec/rail_feeds/network_rail/schedule/tiploc_spec.rb +77 -0
- data/spec/rail_feeds/network_rail/schedule/train_schedule/change_en_route_spec.rb +121 -0
- data/spec/rail_feeds/network_rail/schedule/train_schedule/location/intermediate_spec.rb +95 -0
- data/spec/rail_feeds/network_rail/schedule/train_schedule/location/origin_spec.rb +87 -0
- data/spec/rail_feeds/network_rail/schedule/train_schedule/location/terminating_spec.rb +81 -0
- data/spec/rail_feeds/network_rail/schedule/train_schedule/location_spec.rb +35 -0
- data/spec/rail_feeds/network_rail/schedule/train_schedule_spec.rb +284 -0
- data/spec/rail_feeds/network_rail/schedule_spec.rb +41 -0
- data/spec/rail_feeds/network_rail/smart_spec.rb +194 -0
- data/spec/rail_feeds/network_rail/stomp_client_spec.rb +151 -0
- data/spec/rail_feeds/network_rail_spec.rb +7 -0
- data/spec/rail_feeds_spec.rb +11 -0
- data/spec/spec_helper.rb +47 -0
- metadata +282 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'open-uri'
|
|
4
|
+
|
|
5
|
+
module RailFeeds
|
|
6
|
+
module NetworkRail
|
|
7
|
+
# A wrapper class for ::Net::HTTP
|
|
8
|
+
class HTTPClient
|
|
9
|
+
include Logging
|
|
10
|
+
|
|
11
|
+
HOST = 'datafeeds.networkrail.co.uk'
|
|
12
|
+
|
|
13
|
+
# Initialize a new http client.
|
|
14
|
+
# @param [RailFeeds::NetworkRail::Credentials] credentials
|
|
15
|
+
# The credentials for connecting to the feed.
|
|
16
|
+
# @param [Logger] logger
|
|
17
|
+
# The logger for outputting evetns, if nil the global logger will be used.
|
|
18
|
+
def initialize(credentials: Credentials, logger: nil)
|
|
19
|
+
@credentials = credentials
|
|
20
|
+
self.logger = logger unless logger.nil?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Fetch path from network rail server.
|
|
24
|
+
# @param [String] path The path to fetch.
|
|
25
|
+
# @yield [file] Once the block has run the temp file will be deleted.
|
|
26
|
+
# @yieldparam [Tempfile] file The content of the file.
|
|
27
|
+
def fetch(path)
|
|
28
|
+
logger.debug "fetch(#{path.inspect})"
|
|
29
|
+
uri = URI("https://#{HOST}/#{path}")
|
|
30
|
+
yield uri.open(http_basic_authentication: @credentials.to_a)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Fetch path from network rail server and unzip it.
|
|
34
|
+
# @param [String] path The path to fetch.
|
|
35
|
+
# @yield [reader] Once the block has run the temp file will be deleted.
|
|
36
|
+
# @yieldparam [Zlib::GzipReader] reader The unzippable content of the file.
|
|
37
|
+
def fetch_unzipped(path)
|
|
38
|
+
logger.debug "get_unzipped(#{path.inspect})"
|
|
39
|
+
fetch(path) do |gz_file|
|
|
40
|
+
yield Zlib::GzipReader.open(gz_file.path)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Download path from netwrok rail server.
|
|
45
|
+
# @param [String] path The path to download.
|
|
46
|
+
# @param [String] file The path to the file to save the contents in.
|
|
47
|
+
def download(path, file)
|
|
48
|
+
logger.debug "download(#{path.inspect}, #{file.inspect})"
|
|
49
|
+
fetch(path) do |src|
|
|
50
|
+
File.open(file, 'w') do |dst|
|
|
51
|
+
IO.copy_stream src, dst
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailFeeds
|
|
4
|
+
module NetworkRail
|
|
5
|
+
module Schedule
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
|
7
|
+
# A class for holding information about an association between many trains.
|
|
8
|
+
class Association
|
|
9
|
+
include Comparable
|
|
10
|
+
include Schedule::Days
|
|
11
|
+
include Schedule::STPIndicator
|
|
12
|
+
|
|
13
|
+
# @!attribute [rw] main_train_uid
|
|
14
|
+
# @return [String] The UID of the main train in the association.
|
|
15
|
+
# @!attribute [rw] associated_train_uid
|
|
16
|
+
# @return [String] The UID of the associated train in the association.
|
|
17
|
+
# @!attribute [rw] category
|
|
18
|
+
# @return [String] The category of the association:
|
|
19
|
+
# * JJ - join
|
|
20
|
+
# * VV - divide
|
|
21
|
+
# * NP - next
|
|
22
|
+
# @!attribute [rw] start_date
|
|
23
|
+
# @return [Date] When the schedule starts.
|
|
24
|
+
# @!attribute [rw] end_date
|
|
25
|
+
# @return [Date] When the schedule ends.
|
|
26
|
+
# @!attribute [rw] date_indicator
|
|
27
|
+
# @return [String] When the assocation happens:
|
|
28
|
+
# * S - same day
|
|
29
|
+
# * N - over next midnight
|
|
30
|
+
# * P - over previous midnight
|
|
31
|
+
# @!attribute [rw] days
|
|
32
|
+
# @return [Array<Boolean>] The days on which the service runs.
|
|
33
|
+
# [Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, Sunday]
|
|
34
|
+
# @!attribute [rw] tiploc
|
|
35
|
+
# @return [String] The TIPLOC of the location the association occurs.
|
|
36
|
+
# @!attribute [rw] base_location_suffix
|
|
37
|
+
# @return [String, nil]
|
|
38
|
+
# Together with the tiploc uniquely identifies the association
|
|
39
|
+
# on the base_uid.
|
|
40
|
+
# @!attribute [rw] associated_location_suffix
|
|
41
|
+
# @return [String, nil]
|
|
42
|
+
# Together with the tiploc uniquely identifies the association
|
|
43
|
+
# on the associated_uid.
|
|
44
|
+
# @!attribute [rw] stp_indicator
|
|
45
|
+
# @return [String]
|
|
46
|
+
# * C - cancellation of permanent schedule
|
|
47
|
+
# * N - new STP schedule
|
|
48
|
+
# * O - STP overlay of permanent schedule
|
|
49
|
+
# * P - permanent
|
|
50
|
+
|
|
51
|
+
attr_accessor :main_train_uid, :associated_train_uid, :category,
|
|
52
|
+
:start_date, :end_date, :date_indicator,
|
|
53
|
+
:tiploc, :main_location_suffix, :associated_location_suffix
|
|
54
|
+
# Attributes from modules :days, :stp_indicator
|
|
55
|
+
|
|
56
|
+
def initialize(**attributes)
|
|
57
|
+
attributes.each do |attribute, value|
|
|
58
|
+
send "#{attribute}=", value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# rubocop:disable Metrics/AbcSize
|
|
63
|
+
# rubocop:disable Metrics/MethodLength
|
|
64
|
+
# Initialize a new association from a CIF file line
|
|
65
|
+
def self.from_cif(line)
|
|
66
|
+
unless %w[AAN AAR AAD].include?(line[0..2])
|
|
67
|
+
fail ArgumentError, "Invalid line:\n#{line}"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
new(
|
|
71
|
+
main_train_uid: line[3..8].strip,
|
|
72
|
+
associated_train_uid: line[9..14].strip,
|
|
73
|
+
start_date: Schedule.make_date(line[15..20]),
|
|
74
|
+
end_date: Schedule.make_date(line[21..26], allow_nil: line[2].eql?('D')),
|
|
75
|
+
days: days_from_cif(line[27..33]),
|
|
76
|
+
category: Schedule.nil_or_strip(line[34..35]),
|
|
77
|
+
date_indicator: Schedule.nil_or_strip(line[36]),
|
|
78
|
+
tiploc: line[37..43].strip,
|
|
79
|
+
main_location_suffix: Schedule.nil_or_i(line[44]),
|
|
80
|
+
associated_location_suffix: Schedule.nil_or_i(line[45]),
|
|
81
|
+
stp_indicator: stp_indicator_from_cif(line[79])
|
|
82
|
+
)
|
|
83
|
+
end
|
|
84
|
+
# rubocop:enable Metrics/AbcSize
|
|
85
|
+
# rubocop:enable Metrics/MethodLength
|
|
86
|
+
|
|
87
|
+
# rubocop:disable Metrics/AbcSize
|
|
88
|
+
# Initialize a new association from a JSON file line
|
|
89
|
+
def self.from_json(line)
|
|
90
|
+
data = ::JSON.parse(line)['JsonAssociationV1']
|
|
91
|
+
|
|
92
|
+
new(
|
|
93
|
+
main_train_uid: data['main_train_uid'],
|
|
94
|
+
associated_train_uid: data['assoc_train_uid'],
|
|
95
|
+
start_date: Date.parse(data['assoc_start_date']),
|
|
96
|
+
end_date: data['assoc_end_date'] ? Date.parse(data['assoc_end_date']) : nil,
|
|
97
|
+
days: days_from_cif(data['assoc_days']),
|
|
98
|
+
category: Schedule.nil_or_strip(data['category']),
|
|
99
|
+
date_indicator: Schedule.nil_or_strip(data['date_indicator']),
|
|
100
|
+
tiploc: data['location'],
|
|
101
|
+
main_location_suffix: Schedule.nil_or_i(data['base_location_suffix']),
|
|
102
|
+
associated_location_suffix: Schedule.nil_or_i(data['assoc_location_suffix']),
|
|
103
|
+
stp_indicator: stp_indicator_from_cif(data['CIF_stp_indicator'])
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
# rubocop:enable Metrics/AbcSize
|
|
107
|
+
|
|
108
|
+
# Test if this is a join association.
|
|
109
|
+
def join?
|
|
110
|
+
category.eql?('JJ')
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
# Test if this is a divide association.
|
|
114
|
+
def divide?
|
|
115
|
+
category.eql?('VV')
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Test if this is a next association.
|
|
119
|
+
def next?
|
|
120
|
+
category.eql?('NP')
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
# Test if the association happens on the same day.
|
|
124
|
+
def same_day?
|
|
125
|
+
date_indicator.eql?('S')
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Test if the association happens over the next midnight.
|
|
129
|
+
def over_next_midnight?
|
|
130
|
+
date_indicator.eql?('N')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Test if the association happens over the previous midnight.
|
|
134
|
+
def over_previous_midnight?
|
|
135
|
+
date_indicator.eql?('P')
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Uniquely identifies the event on the main_train_uid
|
|
139
|
+
def main_train_event_id
|
|
140
|
+
"#{tiploc}-#{main_location_suffix}"
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# Uniquely identifies the event on the associated_train_uid
|
|
144
|
+
def associated_train_event_id
|
|
145
|
+
"#{tiploc}-#{associated_location_suffix}"
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def ==(other)
|
|
149
|
+
main_train_event_id == other&.main_train_event_id &&
|
|
150
|
+
associated_train_event_id == other&.associated_train_event_id
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def <=>(other)
|
|
154
|
+
start_date <=> other&.start_date
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def hash
|
|
158
|
+
"#{tiploc}-#{main_location_suffix}-#{associated_location_suffix}"
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
# rubocop:disable Metrics/AbcSize
|
|
162
|
+
def to_cif
|
|
163
|
+
format('%-80.80s', [
|
|
164
|
+
'AAN',
|
|
165
|
+
format('%-6.6s', main_train_uid),
|
|
166
|
+
format('%-6.6s', associated_train_uid),
|
|
167
|
+
# rubocop:disable Style/FormatStringToken
|
|
168
|
+
format('%-6.6s', start_date&.strftime('%y%m%d')),
|
|
169
|
+
format('%-6.6s', end_date&.strftime('%y%m%d')),
|
|
170
|
+
# rubocop:enable Style/FormatStringToken
|
|
171
|
+
days_to_cif,
|
|
172
|
+
format('%-2.2s', category),
|
|
173
|
+
format('%-1.1s', date_indicator),
|
|
174
|
+
format('%-7.7s', tiploc),
|
|
175
|
+
format('%-1.1s', main_location_suffix),
|
|
176
|
+
format('%-1.1s', associated_location_suffix),
|
|
177
|
+
'T ',
|
|
178
|
+
stp_indicator_to_cif
|
|
179
|
+
].join) + "\n"
|
|
180
|
+
end
|
|
181
|
+
# rubocop:enable Metrics/AbcSize
|
|
182
|
+
|
|
183
|
+
# rubocop:disable Metrics/MethodLength
|
|
184
|
+
def to_json
|
|
185
|
+
{
|
|
186
|
+
'JsonAssociationV1' => {
|
|
187
|
+
'transaction_type' => 'Create',
|
|
188
|
+
'main_train_uid' => main_train_uid,
|
|
189
|
+
'assoc_train_uid' => associated_train_uid,
|
|
190
|
+
'assoc_start_date' => start_date.strftime('%Y-%m-%dT00:00:00Z'),
|
|
191
|
+
'assoc_end_date' => end_date.strftime('%Y-%m-%dT00:00:00Z'),
|
|
192
|
+
'assoc_days' => days_to_cif,
|
|
193
|
+
'category' => category,
|
|
194
|
+
'date_indicator' => date_indicator,
|
|
195
|
+
'location' => tiploc,
|
|
196
|
+
'base_location_suffix' => main_location_suffix,
|
|
197
|
+
'assoc_location_suffix' => associated_location_suffix,
|
|
198
|
+
'diagram_type' => 'T',
|
|
199
|
+
'CIF_stp_indicator' => stp_indicator_to_cif
|
|
200
|
+
}
|
|
201
|
+
}.to_json
|
|
202
|
+
end
|
|
203
|
+
# rubocop:enable Metrics/MethodLength
|
|
204
|
+
end
|
|
205
|
+
# rubocop:enable Metrics/ClassLength
|
|
206
|
+
end
|
|
207
|
+
end
|
|
208
|
+
end
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailFeeds
|
|
4
|
+
module NetworkRail
|
|
5
|
+
module Schedule
|
|
6
|
+
# rubocop:disable Metrics/ClassLength
|
|
7
|
+
# A class for holding schedule data read from schedule file(s).
|
|
8
|
+
class Data
|
|
9
|
+
include Logging
|
|
10
|
+
|
|
11
|
+
# @!attribute [r] last_header The last header added.
|
|
12
|
+
# @return [RailFeeds::NetworkRail::Schedule::Header::CIF]
|
|
13
|
+
# @!attribute [r] associations
|
|
14
|
+
# @return [Hash<RailFeeds::NetworkRail::Schedule::Association>]
|
|
15
|
+
# @!attribute [r] tiplocs
|
|
16
|
+
# @return [Hash<RailFeeds::NetworkRail::Schedule::Tiploc>]
|
|
17
|
+
# @!attribute [r] trains
|
|
18
|
+
# @return [Hash{String=>RailFeeds::NetworkRail::Schedule::TrainSchedule}]
|
|
19
|
+
# Schedules grouped by the train's UID
|
|
20
|
+
|
|
21
|
+
attr_accessor :last_header, :associations, :tiplocs, :trains
|
|
22
|
+
|
|
23
|
+
# rubocop:disable Metrics/AbcSize
|
|
24
|
+
# rubocop:disable Metrics/MethodLength
|
|
25
|
+
# Initialize a new data.
|
|
26
|
+
# @param [Logger, nil] logger
|
|
27
|
+
# The logger for outputting events, if nil the global logger is used.
|
|
28
|
+
def initialize(logger: nil)
|
|
29
|
+
self.logger = logger unless logger.nil?
|
|
30
|
+
@parser = Parser::CIF.new(
|
|
31
|
+
logger: logger,
|
|
32
|
+
on_header: proc { |*args| do_header(*args) },
|
|
33
|
+
on_trailer: proc { |*args| do_trailer(*args) },
|
|
34
|
+
on_tiploc_create: proc { |*args| do_tiploc_create(*args) },
|
|
35
|
+
on_tiploc_update: proc { |*args| do_tiploc_update(*args) },
|
|
36
|
+
on_tiploc_delete: proc { |*args| do_tiploc_delete(*args) },
|
|
37
|
+
on_association_create: proc { |*args| do_association_create(*args) },
|
|
38
|
+
on_association_update: proc { |*args| do_association_update(*args) },
|
|
39
|
+
on_association_delete: proc { |*args| do_association_delete(*args) },
|
|
40
|
+
on_train_schedule_create: proc { |*args| do_train_schedule_create(*args) },
|
|
41
|
+
on_train_schedule_update: proc { |*args| do_train_schedule_update(*args) },
|
|
42
|
+
on_train_schedule_delete: proc { |*args| do_train_schedule_delete(*args) }
|
|
43
|
+
)
|
|
44
|
+
reset_data
|
|
45
|
+
end
|
|
46
|
+
# rubocop:enable Metrics/AbcSize
|
|
47
|
+
# rubocop:enable Metrics/MethodLength
|
|
48
|
+
|
|
49
|
+
# Load data files into the parser, of types:
|
|
50
|
+
# * Full CIF file - the data will be replaced
|
|
51
|
+
# * Update CIF file - the data will be changed
|
|
52
|
+
# @param [IO] file
|
|
53
|
+
# The file to load data from.
|
|
54
|
+
def load_cif_file(file)
|
|
55
|
+
@parser.parse_cif_file file
|
|
56
|
+
|
|
57
|
+
logger.info "Currently have #{associations.count} associations, " \
|
|
58
|
+
"#{tiplocs.count} tiplocs, #{trains.count} trains."
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# rubocop:disable Metrics/AbcSize
|
|
62
|
+
# rubocop:disable Metrics/MethodLength
|
|
63
|
+
# Get the contained data in CIF format
|
|
64
|
+
# Expects a block to receive each line
|
|
65
|
+
def generate_cif
|
|
66
|
+
fail 'No loaded data' if last_header.nil?
|
|
67
|
+
|
|
68
|
+
header = Header::CIF.new(
|
|
69
|
+
extracted_at: last_header.extracted_at,
|
|
70
|
+
update_indicator: 'F',
|
|
71
|
+
start_date: last_header.start_date,
|
|
72
|
+
end_date: last_header.end_date
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
yield "/!! Start of file\n"
|
|
76
|
+
yield "/!! Generated: #{header.extracted_at.utc&.strftime('%d/%m/%Y %H:%M')}\n"
|
|
77
|
+
yield header.to_cif
|
|
78
|
+
tiplocs.values.sort.each { |tiploc| yield tiploc.to_cif }
|
|
79
|
+
associations.values.sort.each { |association| yield association.to_cif }
|
|
80
|
+
trains.values.flatten.sort.each do |train_schedule|
|
|
81
|
+
train_schedule.to_cif.each_line { |line| yield line }
|
|
82
|
+
end
|
|
83
|
+
yield "ZZ#{' ' * 78}\n"
|
|
84
|
+
yield "/!! End of file\n"
|
|
85
|
+
end
|
|
86
|
+
# rubocop:enable Metrics/AbcSize
|
|
87
|
+
# rubocop:enable Metrics/MethodLength
|
|
88
|
+
|
|
89
|
+
# Fetch data over the web.
|
|
90
|
+
# Gets the feed of all trains.
|
|
91
|
+
# @param [RailFeeds::NetworkRail::Credentials] credentials
|
|
92
|
+
# The credentials for connecting to the feed.
|
|
93
|
+
# @return [RailFeeds::NetworkRail::Schedule::Header::CIF]
|
|
94
|
+
# The header of the last file added.
|
|
95
|
+
def fetch_data(credentials: Credentials)
|
|
96
|
+
fetcher = Fetcher.new credentials: credentials
|
|
97
|
+
|
|
98
|
+
method = if last_header.nil? ||
|
|
99
|
+
last_header.extracted_at.to_date < Date.today - 6
|
|
100
|
+
# Need to get a full andthen updates
|
|
101
|
+
:fetch_all
|
|
102
|
+
else
|
|
103
|
+
# Can only get updates
|
|
104
|
+
:fetch_all_updates
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
fetcher.send(method, :cif) do |file|
|
|
108
|
+
load_cif_file file
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
private
|
|
113
|
+
|
|
114
|
+
def reset_data
|
|
115
|
+
@last_header = nil
|
|
116
|
+
@associations = {}
|
|
117
|
+
@tiplocs = {}
|
|
118
|
+
@trains = {}
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# rubocop:disable Metrics/AbcSize
|
|
122
|
+
# rubocop:disable Metrics/CyclomaticComplexity
|
|
123
|
+
# rubocop:disable Metrics/MethodLength
|
|
124
|
+
# rubocop:disable Metrics/PerceivedComplexity
|
|
125
|
+
def ensure_correct_update_order(header)
|
|
126
|
+
if last_header.nil?
|
|
127
|
+
# No data whatsoever - this must be a full extract
|
|
128
|
+
unless header.full?
|
|
129
|
+
fail ArgumentError,
|
|
130
|
+
'Update can\'t be loaded before loading a full extract.'
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
elsif last_header.update? && header.update?
|
|
134
|
+
# Check against last update
|
|
135
|
+
if header.extracted_at < last_header.extracted_at
|
|
136
|
+
fail ArgumentError,
|
|
137
|
+
'Update is too old, it is before the last applied update.'
|
|
138
|
+
end
|
|
139
|
+
if header.previous_file_reference != last_header.current_file_reference
|
|
140
|
+
fail ArgumentError,
|
|
141
|
+
'Missing update(s). Last applied update is ' \
|
|
142
|
+
"#{last_header.current_file_reference.inspect}, " \
|
|
143
|
+
"this update requires #{header.previous_file_reference.inspect} " \
|
|
144
|
+
'to be the previous applied update.'
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
# rubocop:enable Metrics/AbcSize
|
|
149
|
+
# rubocop:enable Metrics/CyclomaticComplexity
|
|
150
|
+
# rubocop:enable Metrics/MethodLength
|
|
151
|
+
# rubocop:enable Metrics/PerceivedComplexity
|
|
152
|
+
|
|
153
|
+
# Header record
|
|
154
|
+
def do_header(_parser, header)
|
|
155
|
+
ensure_correct_update_order header
|
|
156
|
+
reset_data if header.full?
|
|
157
|
+
@last_header = header
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
# TIPLOC Insert record
|
|
161
|
+
def do_tiploc_create(_parser, tiploc)
|
|
162
|
+
tiplocs[tiploc.hash] = tiploc
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# TIPLOC Amend record
|
|
166
|
+
def do_tiploc_update(_parser, tiploc_id, tiploc)
|
|
167
|
+
tiplocs[tiploc_id] = tiploc
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# TIPLOC Delete record
|
|
171
|
+
def do_tiploc_delete(_parser, tiploc)
|
|
172
|
+
tiplocs.delete tiploc.hash
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
# Association New record
|
|
176
|
+
def do_association_create(_parser, association)
|
|
177
|
+
associations[association.hash] = association
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Association Revise record
|
|
181
|
+
def do_association_update(_parser, association)
|
|
182
|
+
associations[association.hash] = association
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
# Association Delete record
|
|
186
|
+
def do_association_delete(_parser, association)
|
|
187
|
+
associations.delete association.hash
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
# New Train received
|
|
191
|
+
def do_train_schedule_create(_parser, train_schedule)
|
|
192
|
+
trains[train_schedule.uid] ||= []
|
|
193
|
+
trains[train_schedule.uid].push train_schedule
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Revise Train received
|
|
197
|
+
def do_train_schedule_update(parser, train_schedule)
|
|
198
|
+
trains[train_schedule.uid] ||= []
|
|
199
|
+
index = trains[train_schedule.uid].index train_schedule
|
|
200
|
+
return do_train_schedule_create(parser, train_schedule) if index.nil?
|
|
201
|
+
trains[train_schedule.uid][index] = train_schedule
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Delete Train record
|
|
205
|
+
def do_train_schedule_delete(_parser, train_schedule)
|
|
206
|
+
trains[train_schedule.uid]&.delete train_schedule
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
# Trailer record
|
|
210
|
+
def do_trailer(_parser); end
|
|
211
|
+
end
|
|
212
|
+
# rubocop:enable Metrics/ClassLength
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module RailFeeds
|
|
4
|
+
module NetworkRail
|
|
5
|
+
module Schedule
|
|
6
|
+
# A collection of methods for working with a days array.
|
|
7
|
+
# Provides a days attribute to the class.
|
|
8
|
+
module Days
|
|
9
|
+
def self.included(base)
|
|
10
|
+
base.extend ClassMethods
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# @return [Array<Boolean, nil>] What days the record applies to
|
|
14
|
+
# (Monday -> Sunday).
|
|
15
|
+
def days
|
|
16
|
+
@days ||= Array.new(7, nil)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
# @param [Array<Boolean, nil>, #to_s] value What days the record applies to.
|
|
20
|
+
# (Monday -> Sunday).
|
|
21
|
+
def days=(value)
|
|
22
|
+
value = days_from_cif(value) unless value.is_a?(Array)
|
|
23
|
+
(0..6).each do |i|
|
|
24
|
+
days[i] = value[i]&.&(true)
|
|
25
|
+
end
|
|
26
|
+
days
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Query if the record applies on Mondays
|
|
30
|
+
# @return [Boolean, nil]
|
|
31
|
+
def mondays?
|
|
32
|
+
days[0]
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Query if the record applies on Tuesdays
|
|
36
|
+
# @return [Boolean, nil]
|
|
37
|
+
def tuesdays?
|
|
38
|
+
days[1]
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Query if the record applies on Wednesdays
|
|
42
|
+
# @return [Boolean, nil]
|
|
43
|
+
def wednesdays?
|
|
44
|
+
days[2]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Query if the record applies on Thursdays
|
|
48
|
+
# @return [Boolean, nil]
|
|
49
|
+
def thursdays?
|
|
50
|
+
days[3]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Query if the record applies on Fridays
|
|
54
|
+
# @return [Boolean, nil]
|
|
55
|
+
def fridays?
|
|
56
|
+
days[4]
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Query if the record applies on Saturdays
|
|
60
|
+
# @return [Boolean, nil]
|
|
61
|
+
def saturdays?
|
|
62
|
+
days[5]
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Query if the record applies on Sundays
|
|
66
|
+
# @return [Boolean, nil]
|
|
67
|
+
def sundays?
|
|
68
|
+
days[6]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
protected
|
|
72
|
+
|
|
73
|
+
def days_to_cif
|
|
74
|
+
self.class.days_to_cif days
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def days_from_cif(value)
|
|
78
|
+
self.days = self.class.days_from_cif value
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
module ClassMethods # :nodoc:
|
|
82
|
+
def days_to_cif(value)
|
|
83
|
+
value.map { |d| d ? '1' : '0' }.join
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def days_from_cif(value)
|
|
87
|
+
return [nil, nil, nil, nil, nil, nil, nil] if value.nil?
|
|
88
|
+
Array.new(7) { |i| value[i]&.eql?('1') }
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
private_constant :ClassMethods
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
end
|