Skip to content

Commit 8f7ce7f

Browse files
authored
NeTEx: Extracteur de Network (#5341)
1 parent 71ec1a5 commit 8f7ce7f

9 files changed

Lines changed: 260 additions & 95 deletions

File tree

apps/transport/lib/netex/calendars_streaming_parser.ex

Lines changed: 2 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,13 @@ defmodule Transport.NeTEx.CalendarsStreamingParser do
55

66
@behaviour Saxy.Handler
77

8-
import Transport.NeTEx.NeTExHelpers
98
import Transport.NeTEx.SaxyHelpers
109

1110
def initial_state do
12-
%{
13-
capture: false,
14-
current_tree: [],
11+
capturing_initial_state(%{
1512
calendars: [],
1613
operating_periods: []
17-
}
14+
})
1815
end
1916

2017
def unwrap_result(final_state), do: final_state.calendars ++ final_state.operating_periods
@@ -83,16 +80,6 @@ defmodule Transport.NeTEx.CalendarsStreamingParser do
8380
update_in(state, [:current_operating_period], &(&1 |> Map.put(field, value)))
8481
end
8582

86-
defp push(state, element), do: state |> update_in([:current_tree], &(&1 ++ [element]))
87-
88-
defp pop(state), do: update_in(state, [:current_tree], &(&1 |> List.delete_at(-1)))
89-
90-
defp reset_tree(state), do: %{state | current_tree: []}
91-
92-
defp start_capture(state), do: %{state | capture: true}
93-
94-
defp stop_capture(state), do: %{state | capture: false}
95-
9683
defp register_calendar(state) do
9784
current = state.current_calendar
9885

apps/transport/lib/netex/netex_archive_parser.ex

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,31 @@ defmodule Transport.NeTEx.ArchiveParser do
116116
read_all(zip_file_name, &read_types_of_frames!/2)
117117
end
118118

119+
@doc """
120+
Inside a zip archive opened with `Unzip`, parse a given file (pointed by
121+
`file_name`) and extract the networks. The file is read in streaming
122+
fashion to save memory, but the stop places are stacked in a list (all in
123+
memory at once).
124+
"""
125+
def read_networks(%Unzip{} = unzip, file_name) do
126+
parse_stream(unzip, file_name, Transport.NeTEx.NetworkParser)
127+
end
128+
129+
@doc """
130+
Like read_networks/2 but raises on errors.
131+
"""
132+
def read_networks!(%Unzip{} = unzip, file_name) do
133+
parse_stream!(unzip, file_name, Transport.NeTEx.NetworkParser)
134+
end
135+
136+
def read_all_networks(zip_file_name) do
137+
read_all(zip_file_name, &read_networks/2)
138+
end
139+
140+
def read_all_networks!(zip_file_name) do
141+
read_all(zip_file_name, &read_networks!/2)
142+
end
143+
119144
defp parse_stream(unzip, file_name, parser) do
120145
extension = Path.extname(file_name)
121146

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
defmodule Transport.NeTEx.NetworkParser do
2+
@moduledoc """
3+
Extract network informations.
4+
"""
5+
6+
@behaviour Saxy.Handler
7+
8+
import Transport.NeTEx.SaxyHelpers
9+
10+
def initial_state do
11+
capturing_initial_state(%{
12+
networks: []
13+
})
14+
end
15+
16+
def unwrap_result(final_state), do: final_state.networks
17+
18+
def handle_event(:start_element, {element, _attributes}, state) do
19+
state =
20+
case {element, state[:capture]} do
21+
{"Network", _} ->
22+
state |> push(element) |> start_capture()
23+
24+
{_, true} ->
25+
state |> push(element)
26+
27+
_ ->
28+
state
29+
end
30+
31+
{:ok, state}
32+
end
33+
34+
def handle_event(:end_element, "Network", state) do
35+
{:ok, state |> stop_capture() |> pop()}
36+
end
37+
38+
def handle_event(:end_element, _, state) do
39+
{:ok, state |> pop()}
40+
end
41+
42+
def handle_event(:characters, chars, state)
43+
when state.capture and state.current_tree == ["Network", "Name"] do
44+
{:ok, state |> register_network(chars)}
45+
end
46+
47+
def handle_event(_, _, state), do: {:ok, state}
48+
49+
defp register_network(state, network), do: update_in(state, [:networks], &(&1 ++ [network]))
50+
end

apps/transport/lib/netex/saxy_helpers.ex

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,21 @@ defmodule Transport.NeTEx.SaxyHelpers do
3737
end
3838
end
3939
end
40+
41+
def capturing_initial_state(initial_state) do
42+
Map.merge(initial_state, %{
43+
capture: false,
44+
current_tree: []
45+
})
46+
end
47+
48+
def push(state, element), do: state |> update_in([:current_tree], &(&1 ++ [element]))
49+
50+
def pop(state), do: update_in(state, [:current_tree], &(&1 |> List.delete_at(-1)))
51+
52+
def reset_tree(state), do: %{state | current_tree: []}
53+
54+
def start_capture(state), do: %{state | capture: true}
55+
56+
def stop_capture(state), do: %{state | capture: false}
4057
end

apps/transport/lib/netex/service_calendars_streaming_parser.ex

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -91,16 +91,6 @@ defmodule Transport.NeTEx.ServiceCalendarsStreamingParser do
9191
update_in(state, [:current_service_calendar], &(&1 |> Map.put(field, value)))
9292
end
9393

94-
defp push(state, element), do: state |> update_in([:current_tree], &(&1 ++ [element]))
95-
96-
defp pop(state), do: update_in(state, [:current_tree], &(&1 |> List.delete_at(-1)))
97-
98-
defp reset_tree(state), do: %{state | current_tree: []}
99-
100-
defp start_capture(state), do: %{state | capture: true}
101-
102-
defp stop_capture(state), do: %{state | capture: false}
103-
10494
defp register_service_calendar(state) do
10595
current = state.current_service_calendar
10696

apps/transport/lib/validators/netex/metadata_extractor.ex

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,13 @@ defmodule Transport.Validators.NeTEx.MetadataExtractor do
44
- start_date and end_date (from calendar and service calendars)
55
"""
66

7+
alias Transport.NeTEx.ArchiveParser
8+
79
def extract(filepath) do
10+
Map.merge(extract_validity_dates(filepath), extract_networks(filepath))
11+
end
12+
13+
def extract_validity_dates(filepath) do
814
case validity_dates(filepath) do
915
{start_date, end_date} ->
1016
%{
@@ -19,6 +25,12 @@ defmodule Transport.Validators.NeTEx.MetadataExtractor do
1925
_ -> no_validity_dates()
2026
end
2127

28+
def extract_networks(filepath) do
29+
%{"networks" => run_parser(filepath, &ArchiveParser.read_all_networks/1)}
30+
rescue
31+
_ -> %{"networks" => []}
32+
end
33+
2234
defp no_validity_dates, do: %{"no_validity_dates" => true}
2335

2436
defp validity_dates(filepath) do
@@ -30,14 +42,16 @@ defmodule Transport.Validators.NeTEx.MetadataExtractor do
3042
end
3143

3244
defp validity_dates_from_calendars(filepath) do
33-
filepath
34-
|> Transport.NeTEx.ArchiveParser.read_all_calendars()
35-
|> flatten()
45+
run_parser(filepath, &ArchiveParser.read_all_calendars/1)
3646
end
3747

3848
defp validity_dates_from_service_calendars(filepath) do
49+
run_parser(filepath, &ArchiveParser.read_all_service_calendars/1)
50+
end
51+
52+
defp run_parser(filepath, parser) do
3953
filepath
40-
|> Transport.NeTEx.ArchiveParser.read_all_service_calendars()
54+
|> parser.()
4155
|> flatten()
4256
end
4357

apps/transport/test/netex/netex_archive_parser_test.exs

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,50 @@ defmodule Transport.NeTEx.ArchiveParserTest do
279279
assert [] == types
280280
end
281281

282+
test "extract Network(s)" do
283+
general_frame = """
284+
<PublicationDelivery xmlns="http://www.netex.org.uk/netex" version="1.04:FR1-NETEX-1.6-1.8">
285+
<PublicationTimestamp>2026-02-02T15:45:04Z</PublicationTimestamp>
286+
<ParticipantRef>FR1_OFFRE</ParticipantRef>
287+
<dataObjects>
288+
<GeneralFrame id="FR:GeneralFrame:NETEX_COMMUN:LOC" version="1.09:FR-NETEX-2.1-1.0">
289+
<members>
290+
<Network>
291+
<Name>Réseau Urbain</Name>
292+
</Network>
293+
<Line>
294+
<Name>Alberville - Besançon</Name>
295+
</Line>
296+
</members>
297+
</GeneralFrame>
298+
</dataObjects>
299+
</PublicationDelivery>
300+
"""
301+
302+
assert ["Réseau Urbain"] == extract(&ArchiveParser.read_all_networks!/1, general_frame)
303+
304+
multiple_networks = """
305+
<PublicationDelivery xmlns="http://www.netex.org.uk/netex" version="1.04:FR1-NETEX-1.6-1.8">
306+
<PublicationTimestamp>2026-02-02T15:45:04Z</PublicationTimestamp>
307+
<ParticipantRef>FR1_OFFRE</ParticipantRef>
308+
<dataObjects>
309+
<GeneralFrame id="FR:GeneralFrame:NETEX_COMMUN:LOC" version="1.09:FR-NETEX-2.1-1.0">
310+
<members>
311+
<Network>
312+
<Name>Réseau Urbain</Name>
313+
</Network>
314+
<Network>
315+
<Name>Réseau Régional</Name>
316+
</Network>
317+
</members>
318+
</GeneralFrame>
319+
</dataObjects>
320+
</PublicationDelivery>
321+
"""
322+
323+
assert ["Réseau Urbain", "Réseau Régional"] == extract(&ArchiveParser.read_all_networks!/1, multiple_networks)
324+
end
325+
282326
defp extract(extractor, xml) do
283327
tmp_file = create_tmp_netex([{"file.xml", xml}])
284328

0 commit comments

Comments
 (0)