@@ -18,7 +18,6 @@ defmodule Transport.IRVE.Validator do
1818 so files that are not strictly valid may be considered as valid without any notice by this function.
1919 If you want to call a strict validator (no preprocessing), use `compute_validation/1` instead.
2020 """
21-
2221 def validate ( path , extension \\ ".csv" ) do
2322 # NOTE: for now, load the body in memory, because refactoring to get full streaming
2423 # is too involved for the current sprint deadline.
@@ -40,4 +39,92 @@ defmodule Transport.IRVE.Validator do
4039 df [ "check_row_valid" ]
4140 |> Explorer.Series . all? ( )
4241 end
42+
43+ @ doc """
44+ Produces a human-actionable summary from the validated DataFrame returned by `compute_validation/1`.
45+ Still an Elixir map, but the content should allow someone to know how to correct an invalid file.
46+
47+ Returns a map with:
48+ - `valid` - whether the file is fully valid
49+ - `valid_row_count` - number of valid rows
50+ - `invalid_row_count` - number of invalid rows
51+ - `column_errors` - map of field name => number of invalid rows for that column (only columns with at least one error)
52+ - `error_samples` - up to 5 sample errors per errored column, each with `id_pdc_itinerance`, `column`, and `value`
53+ """
54+ def summarize ( % Explorer.DataFrame { } = df ) do
55+ { valid_count , invalid_count } = summarize_total_counts ( df )
56+ column_errors = summarize_column_errors ( df )
57+ error_samples = error_samples ( df , column_errors )
58+
59+ % Transport.IRVE.Validator.Summary {
60+ valid: invalid_count == 0 ,
61+ valid_row_count: valid_count ,
62+ invalid_row_count: invalid_count ,
63+ total_row_count: valid_count + invalid_count ,
64+ file_level_errors: [ ] ,
65+ column_errors: column_errors ,
66+ error_samples: error_samples
67+ }
68+ end
69+
70+ @ doc """
71+ Combines `validate/2` and `summarize/1` into a single call, catching any file-level error
72+ (bad encoding, wrong format, missing columns, etc.) instead of raising.
73+
74+ Returns the same map as `summarize/1` with an additional `file_level_error` key:
75+ - `nil` when validation ran successfully (the file could still be invalid row-by-row)
76+ - an error message string when a hard file-level error was caught
77+
78+ On a file-level error, `valid_row_count`, `invalid_row_count`, `column_errors`, and
79+ `error_samples` are all `nil`/empty since there is no DataFrame to summarize from.
80+ """
81+ def validate_and_summarize ( path , extension \\ ".csv" ) do
82+ path
83+ |> validate ( extension )
84+ |> summarize ( )
85+ rescue
86+ error ->
87+ % Transport.IRVE.Validator.Summary {
88+ valid: false ,
89+ valid_row_count: nil ,
90+ invalid_row_count: nil ,
91+ total_row_count: nil ,
92+ file_level_errors: [ Exception . message ( error ) ] ,
93+ column_errors: % { } ,
94+ error_samples: [ ]
95+ }
96+ end
97+
98+ defp summarize_total_counts ( df ) do
99+ valid_count = df [ "check_row_valid" ] |> Explorer.Series . sum ( )
100+ invalid_count = Explorer.DataFrame . n_rows ( df ) - valid_count
101+ { valid_count , invalid_count }
102+ end
103+
104+ defp summarize_column_errors ( df ) do
105+ Transport.IRVE.StaticIRVESchema . field_names_list ( )
106+ |> Enum . map ( fn field_name -> { field_name , "check_column_#{ field_name } _valid" } end )
107+ |> Enum . flat_map ( fn { field_name , check_col } ->
108+ error_count = df [ check_col ] |> Explorer.Series . not ( ) |> Explorer.Series . sum ( )
109+ if error_count > 0 , do: [ { field_name , error_count } ] , else: [ ]
110+ end )
111+ |> Map . new ( )
112+ end
113+
114+ defp error_samples ( df , column_errors ) do
115+ column_errors
116+ |> Enum . flat_map ( fn { field_name , _error_count } ->
117+ check_col = "check_column_#{ field_name } _valid"
118+
119+ df
120+ |> Explorer.DataFrame . filter_with ( & ( & 1 [ check_col ] |> Explorer.Series . not ( ) ) )
121+ |> Explorer.DataFrame . select ( [ "id_pdc_itinerance" , field_name ] )
122+ # Limit to 5 samples per error column
123+ |> Explorer.DataFrame . head ( 5 )
124+ |> Explorer.DataFrame . to_rows ( )
125+ |> Enum . map ( fn row ->
126+ % { id_pdc_itinerance: row [ "id_pdc_itinerance" ] , column: field_name , value: row [ field_name ] }
127+ end )
128+ end )
129+ end
43130end
0 commit comments