rail_feeds 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +23 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +31 -0
  5. data/.travis.yml +26 -0
  6. data/CHANGELOG.md +3 -0
  7. data/Gemfile +6 -0
  8. data/Guardfile +25 -0
  9. data/LICENSE.md +32 -0
  10. data/README.md +77 -0
  11. data/Rakefile +3 -0
  12. data/doc/guides/Logging.md +13 -0
  13. data/doc/guides/Network Rail/CORPUS.md +34 -0
  14. data/doc/guides/Network Rail/SMART.md +39 -0
  15. data/doc/guides/Network Rail/Schedule.md +138 -0
  16. data/file +0 -0
  17. data/lib/rail_feeds/credentials.rb +45 -0
  18. data/lib/rail_feeds/logging.rb +51 -0
  19. data/lib/rail_feeds/network_rail/corpus.rb +77 -0
  20. data/lib/rail_feeds/network_rail/credentials.rb +22 -0
  21. data/lib/rail_feeds/network_rail/http_client.rb +57 -0
  22. data/lib/rail_feeds/network_rail/schedule/association.rb +208 -0
  23. data/lib/rail_feeds/network_rail/schedule/data.rb +215 -0
  24. data/lib/rail_feeds/network_rail/schedule/days.rb +95 -0
  25. data/lib/rail_feeds/network_rail/schedule/fetcher.rb +193 -0
  26. data/lib/rail_feeds/network_rail/schedule/header/cif.rb +102 -0
  27. data/lib/rail_feeds/network_rail/schedule/header/json.rb +79 -0
  28. data/lib/rail_feeds/network_rail/schedule/header.rb +22 -0
  29. data/lib/rail_feeds/network_rail/schedule/parser/cif.rb +141 -0
  30. data/lib/rail_feeds/network_rail/schedule/parser/json.rb +87 -0
  31. data/lib/rail_feeds/network_rail/schedule/parser.rb +108 -0
  32. data/lib/rail_feeds/network_rail/schedule/stp_indicator.rb +72 -0
  33. data/lib/rail_feeds/network_rail/schedule/tiploc.rb +100 -0
  34. data/lib/rail_feeds/network_rail/schedule/train_schedule/change_en_route.rb +158 -0
  35. data/lib/rail_feeds/network_rail/schedule/train_schedule/location/intermediate.rb +119 -0
  36. data/lib/rail_feeds/network_rail/schedule/train_schedule/location/origin.rb +91 -0
  37. data/lib/rail_feeds/network_rail/schedule/train_schedule/location/terminating.rb +72 -0
  38. data/lib/rail_feeds/network_rail/schedule/train_schedule/location.rb +76 -0
  39. data/lib/rail_feeds/network_rail/schedule/train_schedule.rb +392 -0
  40. data/lib/rail_feeds/network_rail/schedule.rb +33 -0
  41. data/lib/rail_feeds/network_rail/smart.rb +186 -0
  42. data/lib/rail_feeds/network_rail/stomp_client.rb +77 -0
  43. data/lib/rail_feeds/network_rail.rb +16 -0
  44. data/lib/rail_feeds/version.rb +14 -0
  45. data/lib/rail_feeds.rb +10 -0
  46. data/rail_feeds.gemspec +32 -0
  47. data/spec/fixtures/network_rail/schedule/data/full.yaml +60 -0
  48. data/spec/fixtures/network_rail/schedule/data/starting.yaml +131 -0
  49. data/spec/fixtures/network_rail/schedule/data/update-gap.yaml +10 -0
  50. data/spec/fixtures/network_rail/schedule/data/update-next.yaml +13 -0
  51. data/spec/fixtures/network_rail/schedule/data/update-old.yaml +10 -0
  52. data/spec/fixtures/network_rail/schedule/data/update.yaml +112 -0
  53. data/spec/fixtures/network_rail/schedule/parser/train_create.json +1 -0
  54. data/spec/fixtures/network_rail/schedule/parser/train_delete.json +1 -0
  55. data/spec/fixtures/network_rail/schedule/train_schedule/json-data.yaml +67 -0
  56. data/spec/rail_feeds/credentials_spec.rb +46 -0
  57. data/spec/rail_feeds/logging_spec.rb +81 -0
  58. data/spec/rail_feeds/network_rail/corpus_spec.rb +92 -0
  59. data/spec/rail_feeds/network_rail/credentials_spec.rb +22 -0
  60. data/spec/rail_feeds/network_rail/http_client_spec.rb +88 -0
  61. data/spec/rail_feeds/network_rail/schedule/association_spec.rb +205 -0
  62. data/spec/rail_feeds/network_rail/schedule/data_spec.rb +219 -0
  63. data/spec/rail_feeds/network_rail/schedule/days_shared.rb +99 -0
  64. data/spec/rail_feeds/network_rail/schedule/days_spec.rb +4 -0
  65. data/spec/rail_feeds/network_rail/schedule/fetcher_spec.rb +228 -0
  66. data/spec/rail_feeds/network_rail/schedule/header/cif_spec.rb +72 -0
  67. data/spec/rail_feeds/network_rail/schedule/header/json_spec.rb +51 -0
  68. data/spec/rail_feeds/network_rail/schedule/header_spec.rb +19 -0
  69. data/spec/rail_feeds/network_rail/schedule/parser/cif_spec.rb +197 -0
  70. data/spec/rail_feeds/network_rail/schedule/parser/json_spec.rb +172 -0
  71. data/spec/rail_feeds/network_rail/schedule/parser_spec.rb +34 -0
  72. data/spec/rail_feeds/network_rail/schedule/stp_indicator_shared.rb +49 -0
  73. data/spec/rail_feeds/network_rail/schedule/stp_indicator_spec.rb +4 -0
  74. data/spec/rail_feeds/network_rail/schedule/tiploc_spec.rb +77 -0
  75. data/spec/rail_feeds/network_rail/schedule/train_schedule/change_en_route_spec.rb +121 -0
  76. data/spec/rail_feeds/network_rail/schedule/train_schedule/location/intermediate_spec.rb +95 -0
  77. data/spec/rail_feeds/network_rail/schedule/train_schedule/location/origin_spec.rb +87 -0
  78. data/spec/rail_feeds/network_rail/schedule/train_schedule/location/terminating_spec.rb +81 -0
  79. data/spec/rail_feeds/network_rail/schedule/train_schedule/location_spec.rb +35 -0
  80. data/spec/rail_feeds/network_rail/schedule/train_schedule_spec.rb +284 -0
  81. data/spec/rail_feeds/network_rail/schedule_spec.rb +41 -0
  82. data/spec/rail_feeds/network_rail/smart_spec.rb +194 -0
  83. data/spec/rail_feeds/network_rail/stomp_client_spec.rb +151 -0
  84. data/spec/rail_feeds/network_rail_spec.rb +7 -0
  85. data/spec/rail_feeds_spec.rb +11 -0
  86. data/spec/spec_helper.rb +47 -0
  87. metadata +282 -0
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'net/http'
4
+
5
+ module RailFeeds
6
+ module NetworkRail
7
+ module Schedule
8
+ # A class for fetching the schedule data files.
9
+ class Fetcher
10
+ include Logging
11
+
12
+ # Initialize a new schedule
13
+ # @param [RailFeeds::NetworkRail::Credentials] credentials
14
+ # The credentials for connecting to the feed.
15
+ # @param [Logger, nil] logger
16
+ # The logger for outputting events, if nil the global logger will be used.
17
+ def initialize(credentials: Credentials, logger: nil)
18
+ @credentials = credentials
19
+ self.logger = logger unless logger.nil?
20
+ end
21
+
22
+ # Download the full schedule.
23
+ # @param [:json, :cif] format
24
+ # The format to download the schedule in.
25
+ # @param [String] file
26
+ # The path to the file to save the .json.gz / .cif.gz download in.
27
+ def download_all_full(format, file)
28
+ download 'ALL', 'full', format, file
29
+ end
30
+
31
+ # Download the daily update to the full schedule.
32
+ # @param [String, #to_s] day
33
+ # The day to get the update schedule for ("mon", "tue", "wed", ...).
34
+ # Defaults to the current day.
35
+ # @param [:json, :cif] format
36
+ # The format to fetch the schedule in.
37
+ # @param [String] file
38
+ # The path to the file to save the .json.gz / .cif.gz download in.
39
+ def download_all_update(day, format, file)
40
+ download 'ALL', day, format, file
41
+ end
42
+
43
+ # Fetch the freight schedule.
44
+ # @param [String] file
45
+ # The path to the file to save the .json.gz download in.
46
+ def download_freight_full(file)
47
+ download 'FREIGHT', 'full', :json, file
48
+ end
49
+
50
+ # Fetch the daily update to the freight schedule.
51
+ # @param [String, #to_s] day
52
+ # The day to get the update schedule for ("mon", "tue", "wed", ...).
53
+ # Defaults to the current day.
54
+ # @param [String] file
55
+ # The path to the file to save the .json.gz download in.
56
+ def download_freight_update(day, file)
57
+ download 'FREIGHT', day, :json, file
58
+ end
59
+
60
+ # Fetch the schedule for a TOC.
61
+ # @param [String, #to_s, nil] toc
62
+ # The TOC to get the schedule for.
63
+ # @param [String] file
64
+ # The path to the file to save the .json.gz download in.
65
+ def download_toc_full(toc, file)
66
+ download toc, 'full', :json, file
67
+ end
68
+
69
+ # Fetch the daily update for a TOC.
70
+ # @param [String, #to_s, nil] toc
71
+ # The TOC to get the schedule for.
72
+ # @param [String, #to_s] day
73
+ # The day to get the update schedule for ("mon", "tue", "wed", ...).
74
+ # Defaults to the current day.
75
+ # @param [String] file
76
+ # The path to the file to save the .json.gz download in.
77
+ def download_toc_update(toc, day, file)
78
+ download toc, day, :json, file
79
+ end
80
+
81
+ # Fetch the full schedule.
82
+ # @param [:json, :cif] format
83
+ # The format to fetch the schedule in.
84
+ # @yield [file] Once the block has run the temp file will be deleted.
85
+ # @yieldparam [Zlib::GzipReader] file The unzippable content of the file.
86
+ def fetch_all_full(format, &block)
87
+ fetch 'ALL', 'full', format, &block
88
+ end
89
+
90
+ # Fetch the daily update to the full schedule.
91
+ # @param [String, #to_s] day
92
+ # The day to get the update schedule for ("mon", "tue", "wed", ...).
93
+ # Defaults to the current day.
94
+ # @param [:json, :cif] format
95
+ # The format to fetch the schedule in.
96
+ # @yield [file] Once the block has run the temp file will be deleted.
97
+ # @yieldparam [Zlib::GzipReader] file The unzippable content of the file.
98
+ def fetch_all_update(day, format, &block)
99
+ fetch 'ALL', day, format, &block
100
+ end
101
+
102
+ # Fetch the freight schedule.
103
+ # @yield [file] Once the block has run the temp file will be deleted.
104
+ # @yieldparam [Zlib::GzipReader] file The unzippable content of the file.
105
+ def fetch_freight_full(&block)
106
+ fetch 'FREIGHT', 'full', :json, &block
107
+ end
108
+
109
+ # Fetch the daily update to the freight schedule.
110
+ # @param [String, #to_s] day
111
+ # The day to get the update schedule for ("mon", "tue", "wed", ...).
112
+ # Defaults to the current day.
113
+ # @yield [file] Once the block has run the temp file will be deleted.
114
+ # @yieldparam [TempFile] file The unzippable content of the file.
115
+ def fetch_freight_update(day, &block)
116
+ fetch 'FREIGHT', day, :json, &block
117
+ end
118
+
119
+ # Fetch the schedule for a TOC.
120
+ # @param [String, #to_s, nil] toc
121
+ # The TOC to get the schedule for.
122
+ # @yield [file] Once the block has run the temp file will be deleted.
123
+ # @yieldparam [Zlib::GzipReader] file The unzippable content of the file.
124
+ def fetch_toc_full(toc, &block)
125
+ fetch toc, 'full', :json, &block
126
+ end
127
+
128
+ # Fetch the daily update for a TOC.
129
+ # @param [String, #to_s, nil] toc
130
+ # The TOC to get the schedule for.
131
+ # @param [String, #to_s] day
132
+ # The day to get the update schedule for ("mon", "tue", "wed", ...).
133
+ # Defaults to the current day.
134
+ # @yield [file] Once the block has run the temp file will be deleted.
135
+ # @yieldparam [Zlib::GzipReader] file The unzippable content of the file.
136
+ def fetch_toc_update(toc, day, &block)
137
+ fetch toc, day, :json, &block
138
+ end
139
+
140
+ private
141
+
142
+ # Fetch a schedule.
143
+ def fetch(toc, day, format, &block)
144
+ path = path_for toc, day, format
145
+ client = HTTPClient.new(credentials: @credentials, logger: logger)
146
+ client.fetch_unzipped(path, &block)
147
+ end
148
+
149
+ # Download a schedule.
150
+ def download(toc, day, format, file)
151
+ path = path_for toc, day, format
152
+ client = HTTPClient.new(credentials: @credentials, logger: logger)
153
+ client.download(path, file)
154
+ end
155
+
156
+ # rubocop:disable Metrics/CyclomaticComplexity
157
+ # rubocop:disable Metrics/MethodLength
158
+ # rubocop:disable Metrics/PerceivedComplexity
159
+ # Get the path for a schedule
160
+ def path_for(toc, day, format)
161
+ toc = "#{toc}_TOC" unless %w[ALL FREIGHT].include?(toc)
162
+
163
+ if format.eql?(:cif)
164
+ unless toc.eql?('ALL')
165
+ fail ArgumentError, 'CIF format is only available for the all schedule'
166
+ end
167
+ else
168
+ unless format.eql?(:json)
169
+ fail ArgumentError, 'format must be either :json or :cif'
170
+ end
171
+ end
172
+
173
+ if day.eql?('full')
174
+ day = 'toc-full'
175
+ type = "CIF_#{toc}_FULL_DAILY"
176
+ else
177
+ unless %w[mon tue wed thu fri sat sun].include?(day)
178
+ fail ArgumentError, 'day is invalid'
179
+ end
180
+ day = "toc-update-#{day}"
181
+ type = "CIF_#{toc}_UPDATE_DAILY"
182
+ end
183
+
184
+ path = "ntrod/CifFileAuthenticate?type=#{type}&day=#{day}"
185
+ format.eql?(:cif) ? "#{path}.CIF.gz" : path
186
+ end
187
+ # rubocop:enable Metrics/CyclomaticComplexity
188
+ # rubocop:enable Metrics/MethodLength
189
+ # rubocop:enable Metrics/PerceivedComplexity
190
+ end
191
+ end
192
+ end
193
+ end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailFeeds
4
+ module NetworkRail
5
+ module Schedule
6
+ module Header
7
+ # A class to hole the information from the header row of a cif file
8
+ class CIF
9
+ # @!attribute [rw] file_identity
10
+ # @!attribute [rw] extracted_at
11
+ # @return [Time] When the BTD extract happened.
12
+ # @!attribute [rw] current_file_reference
13
+ # @return [String] Unique reference for the current file.
14
+ # @!attribute [rw] previous_file_reference
15
+ # @return [String, nil] Unique reference for the previous file
16
+ # (the one to apply the update to).
17
+ # @!attribute [rw] update_indicator
18
+ # @return [String] 'F' for a full extract, 'U' for an update extract.
19
+ # @!attribute [rw] version
20
+ # @return [String] The version of the software that generated the CIF file.
21
+ # @!attribute [rw] start_date
22
+ # @return [Date]
23
+ # @!attribute [rw] end_date
24
+ # @return [Date]
25
+
26
+ attr_accessor :file_identity, :extracted_at,
27
+ :current_file_reference, :previous_file_reference,
28
+ :update_indicator, :version, :start_date, :end_date
29
+
30
+ def initialize(**attributes)
31
+ attributes.each do |attribute, value|
32
+ send "#{attribute}=", value
33
+ end
34
+ end
35
+
36
+ # rubocop:disable Metrics/AbcSize
37
+ # Initialize a new header from a CIF file line
38
+ def self.from_cif(line)
39
+ fail ArgumentError, "Invalid line:\n#{line}" unless line[0..1].eql?('HD')
40
+
41
+ new(
42
+ file_identity: line[2..21].strip,
43
+ extracted_at: Time.strptime(line[22..31] + 'UTC', '%d%m%y%H%M%Z'),
44
+ current_file_reference: line[32..38].strip,
45
+ previous_file_reference: line[39..45].strip,
46
+ update_indicator: line[46].strip,
47
+ version: line[47].strip,
48
+ start_date: Date.strptime(line[48..53], '%d%m%y'),
49
+ end_date: Date.strptime(line[54..59], '%d%m%y')
50
+ )
51
+ end
52
+ # rubocop:enable Metrics/AbcSize
53
+
54
+ # Test if this is a header for an update file
55
+ def update?
56
+ update_indicator.eql?('U')
57
+ end
58
+
59
+ # Test if this is a header for a full file
60
+ def full?
61
+ update_indicator.eql?('F')
62
+ end
63
+
64
+ def ==(other)
65
+ hash == other&.hash
66
+ end
67
+
68
+ def hash
69
+ current_file_reference&.dup
70
+ end
71
+
72
+ # rubocop:disable Metrics/AbcSize
73
+ def to_cif
74
+ format('%-80.80s', [
75
+ 'HD',
76
+ format('%-20.20s', file_identity),
77
+ # rubocop:disable Style/FormatStringToken
78
+ format('%-10.10s', extracted_at&.strftime('%d%m%y%H%M')),
79
+ # rubocop:enable Style/FormatStringToken
80
+ format('%-7.7s', current_file_reference),
81
+ format('%-7.7s', previous_file_reference),
82
+ format('%-1.1s', update_indicator),
83
+ format('%-1.1s', version),
84
+ # rubocop:disable Style/FormatStringToken
85
+ format('%-6.6s', start_date&.strftime('%d%m%y')),
86
+ format('%-6.6s', end_date&.strftime('%d%m%y'))
87
+ # rubocop:enable Style/FormatStringToken
88
+ ].join) + "\n"
89
+ end
90
+ # rubocop:enable Metrics/AbcSize
91
+
92
+ def to_s
93
+ "File #{file_identity.inspect} (version #{version}) " \
94
+ "at #{extracted_at.strftime('%Y-%m-%d %H:%M')}. " \
95
+ "#{full? ? 'A full' : 'An update'} extract " \
96
+ "for #{start_date} to #{end_date}."
97
+ end
98
+ end
99
+ end
100
+ end
101
+ end
102
+ end
@@ -0,0 +1,79 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailFeeds
4
+ module NetworkRail
5
+ module Schedule
6
+ module Header
7
+ # A class to hole the information from the header row of a json file
8
+ class JSON
9
+ # @!attribute [rw] extracted_at
10
+ # @return [Time] When the BTD extract happened.
11
+ # @!attribute [rw] sequence
12
+ # @return [Integer] Where this file appears in the sequence of extracts.
13
+ # (Appears to be days since 2012-06-13)
14
+ # @!attribute [r] start_date
15
+ # @return [Date] Infered from sequence
16
+
17
+ attr_accessor :extracted_at, :sequence
18
+
19
+ START_DATE = Date.new 2012, 6, 13
20
+ private_constant :START_DATE
21
+
22
+ def initialize(**attributes)
23
+ attributes.each do |attribute, value|
24
+ send "#{attribute}=", value
25
+ end
26
+ end
27
+
28
+ # Initialize a new header from a JSON file line
29
+ def self.from_json(line)
30
+ data = ::JSON.parse(line)['JsonTimetableV1']
31
+ metadata = data['Metadata']
32
+
33
+ new(
34
+ extracted_at: Time.strptime(data['timestamp'].to_s, '%s').utc,
35
+ sequence: metadata['sequence']
36
+ )
37
+ end
38
+
39
+ def start_date
40
+ START_DATE + sequence.to_i
41
+ end
42
+
43
+ def <=>(other)
44
+ hash <=> other&.hash
45
+ end
46
+
47
+ def hash
48
+ sequence&.dup
49
+ end
50
+
51
+ # rubocop:disable Metrics/MethodLength
52
+ def to_json
53
+ {
54
+ 'JsonTimetableV1' => {
55
+ 'classification' => 'public',
56
+ 'timestamp' => extracted_at.to_i,
57
+ 'owner' => 'Network Rail',
58
+ 'Sender' => {
59
+ 'organisation': '',
60
+ 'application' => 'NTROD',
61
+ 'component' => 'SCHEDULE'
62
+ },
63
+ 'Metadata' => {
64
+ 'type' => 'full',
65
+ 'sequence' => sequence
66
+ }
67
+ }
68
+ }.to_json
69
+ end
70
+ # rubocop:enable Metrics/MethodLength
71
+
72
+ def to_s
73
+ "Sequence #{sequence}, proabbly from #{start_date}."
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
79
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'header/cif'
4
+ require_relative 'header/json'
5
+
6
+ module RailFeeds
7
+ module NetworkRail
8
+ module Schedule
9
+ module Header # :nodoc:
10
+ # Initialize a new header from a CIF file line
11
+ def self.from_cif(line)
12
+ CIF.from_cif line
13
+ end
14
+
15
+ # Initialize a new header from a JSON file line
16
+ def self.from_json(line)
17
+ JSON.from_json line
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,141 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RailFeeds
4
+ module NetworkRail
5
+ module Schedule
6
+ class Parser
7
+ # A class for parsing schedule data read from CIF schedule file(s).
8
+ class CIF < Parser
9
+ UNDERSTOOD_ROWS = %w[
10
+ HD TI TA TD AAN AAD AAR BSN BSD BSR BX LO LI LT CR ZZ
11
+ ].freeze
12
+
13
+ # Parse the data on a single CIF line
14
+ # @param [String] line
15
+ def parse_line(line)
16
+ catch :line_parsed do
17
+ UNDERSTOOD_ROWS.each do |record_type|
18
+ if line.start_with?(record_type)
19
+ send "parse_#{record_type.downcase}_line", line.chomp
20
+ throw :line_parsed
21
+ end
22
+ end
23
+
24
+ if line[0].eql?('/')
25
+ parse_comment_line line.chomp
26
+ throw :line_parsed
27
+ end
28
+
29
+ logger.error "Can't understand line: #{line.chomp.inspect}"
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ # Header record
36
+ def parse_hd_line(line)
37
+ header = Header.from_cif(line)
38
+ logger.info "Starting Parse. #{header}"
39
+ @on_header&.call self, header
40
+ end
41
+
42
+ # TIPLOC Insert record
43
+ def parse_ti_line(line)
44
+ @on_tiploc_create&.call self, Tiploc.from_cif(line)
45
+ end
46
+
47
+ # TIPLOC Amend record
48
+ def parse_ta_line(line)
49
+ tiploc = Tiploc.from_cif(line)
50
+ old_id = tiploc.tiploc
51
+ tiploc.tiploc = line[2..8].strip
52
+ @on_tiploc_update&.call self, old_id, tiploc
53
+ end
54
+
55
+ # TIPLOC Delete record
56
+ def parse_td_line(line)
57
+ @on_tiploc_delete&.call self, Tiploc.from_cif(line).tiploc
58
+ end
59
+
60
+ # Association New record
61
+ def parse_aan_line(line)
62
+ @on_association_create&.call self, Association.from_cif(line)
63
+ end
64
+
65
+ # Association Revise record
66
+ def parse_aar_line(line)
67
+ @on_association_update&.call self, Association.from_cif(line)
68
+ end
69
+
70
+ # Association Delete record
71
+ def parse_aad_line(line)
72
+ @on_association_delete&.call self, Association.from_cif(line)
73
+ end
74
+
75
+ # Train schedule record - basic schedule - new
76
+ def parse_bsn_line(line)
77
+ finish_current_train
78
+ @current_train = TrainSchedule.new
79
+ @current_train.update_from_cif line
80
+ @current_train_action = :create
81
+ end
82
+
83
+ # Train schedule record - basic schedule - delete
84
+ def parse_bsd_line(line)
85
+ finish_current_train
86
+ train = TrainSchedule.new
87
+ train.update_from_cif line
88
+ @on_train_schedule_delete&.call self, train
89
+ end
90
+
91
+ # Train schedule record - basic schedule - revise
92
+ def parse_bsr_line(line)
93
+ finish_current_train
94
+ @current_train = TrainSchedule.new
95
+ @current_train.update_from_cif line
96
+ @current_train_action = :update
97
+ end
98
+
99
+ # Train schedule record - basic schedule extra details
100
+ def parse_bx_line(line)
101
+ @current_train.update_from_cif line
102
+ end
103
+
104
+ # Train schedule record - origin location
105
+ alias parse_lo_line parse_bx_line
106
+ # Train schedule record - intermediate location
107
+ alias parse_li_line parse_bx_line
108
+ # Train schedule record - change en route
109
+ alias parse_cr_line parse_bx_line
110
+ # Train schedule record - terminating location
111
+ alias parse_lt_line parse_bx_line
112
+
113
+ def finish_current_train
114
+ return if @current_train.nil?
115
+
116
+ case @current_train_action
117
+ when :create
118
+ @on_train_schedule_create&.call self, @current_train
119
+ when :update
120
+ @on_train_schedule_update&.call self, @current_train
121
+ end
122
+
123
+ @current_train = nil
124
+ end
125
+
126
+ # Trailer record
127
+ def parse_zz_line(_line)
128
+ finish_current_train
129
+ @file_ended = true
130
+ @on_trailer&.call self
131
+ end
132
+
133
+ # Comment
134
+ def parse_comment_line(line)
135
+ @on_comment&.call self, line[1..-1]
136
+ end
137
+ end
138
+ end
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,87 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module RailFeeds
6
+ module NetworkRail
7
+ module Schedule
8
+ class Parser
9
+ # A class for parsing schedule data read from JSON schedule file(s).
10
+ class JSON < Parser
11
+ def parse_line(line)
12
+ if line.start_with? '{"TiplocV1":'
13
+ parse_tiploc_line line
14
+ elsif line.start_with? '{"JsonAssociationV1":'
15
+ parse_association_line line
16
+ elsif line.start_with? '{"JsonScheduleV1":'
17
+ parse_schedule_line line
18
+ elsif line.start_with? '{"JsonTimetableV1":'
19
+ parse_header_line line
20
+ elsif line.start_with? '{"EOF":'
21
+ parse_trailer_line line
22
+ else
23
+ logger.error "Can't understand line: #{line.chomp}"
24
+ end
25
+ end
26
+
27
+ private
28
+
29
+ def parse_header_line(line)
30
+ header = Header.from_json(line)
31
+ logger.info "Starting Parse. #{header}"
32
+ @on_header&.call self, header
33
+ end
34
+
35
+ def parse_trailer_line(_line)
36
+ @file_ended = true
37
+ @on_trailer&.call self
38
+ end
39
+
40
+ def parse_tiploc_line(line)
41
+ hash = ::JSON.parse(line)['TiplocV1']
42
+
43
+ case hash['transaction_type'].downcase
44
+ when 'create'
45
+ @on_tiploc_create&.call self, Tiploc.from_json(line)
46
+ when 'delete'
47
+ @on_tiploc_delete&.call self, hash['tiploc_code']
48
+ else
49
+ logger.error 'Don\'t know how to ' \
50
+ "#{hash['transaction_type'].inspect} a Tiploc: #{line.chomp}"
51
+ end
52
+ end
53
+
54
+ def parse_association_line(line)
55
+ hash = ::JSON.parse(line)['JsonAssociationV1']
56
+
57
+ case hash['transaction_type'].downcase
58
+ when 'create'
59
+ @on_association_create&.call self, Association.from_json(line)
60
+ when 'delete'
61
+ @on_association_delete&.call self, Association.from_json(line)
62
+ else
63
+ logger.error 'Don\'t know how to ' \
64
+ "#{hash['transaction_type'].inspect} an Association: " \
65
+ "#{line.chomp}"
66
+ end
67
+ end
68
+
69
+ def parse_schedule_line(line)
70
+ hash = ::JSON.parse(line)['JsonScheduleV1']
71
+
72
+ case hash['transaction_type'].downcase
73
+ when 'create'
74
+ @on_train_schedule_create&.call self, TrainSchedule.from_json(line)
75
+ when 'delete'
76
+ @on_train_schedule_delete&.call self, TrainSchedule.from_json(line)
77
+ else
78
+ logger.error 'Don\'t know how to ' \
79
+ "#{hash['transaction_type'].inspect} a Train Schedule: " \
80
+ "#{line.chomp}"
81
+ end
82
+ end
83
+ end
84
+ end
85
+ end
86
+ end
87
+ end