rail_feeds 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- 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
|