Class: ActiveSanction::Parsers::DelimitedTable
- Inherits:
-
Object
- Object
- ActiveSanction::Parsers::DelimitedTable
- Extended by:
- T::Sig
- Defined in:
- lib/active_sanction/parsers/delimited_table.rb,
lib/active_sanction/parsers/delimited_table/row.rb,
lib/active_sanction/parsers/delimited_table/reader.rb
Overview
Reads a record-oriented delimited list -- CSV, TSV, anything stdlib CSV can be told about -- into rows an adapter can map onto Entities.
A table is a description of the file, built once and reused for every sync; a Reader is one pass over one payload.
SDN = ActiveSanction::Parsers::DelimitedTable.new(
columns: %i[ent_num sdn_name sdn_type program title remarks],
null: "-0-",
encoding: Encoding::WINDOWS_1252
)
SDN.read(bytes).each { |row| row[:sdn_name] }
Headerless files, and why columns are declared
OFAC ships all three of its files with no header row, so the names have to come from somewhere. Declaring them in the adapter also pins the file's shape: if OFAC inserts a column, rows arrive the wrong width and every one of them says so in #warnings, which is a far better failure than 19,321 entities quietly built from shifted fields.
A file that does carry a header is read with columns: nil, and the
names come from its first row.
The null sentinel
OFAC does not leave a field empty; it writes -0- , with a trailing
space, and it does this roughly a quarter of a million times:
36,"AEROCARIBBEAN AIRLINES",-0- ,"CUBA",-0- ,-0- ,...
Any declared sentinel is matched after stripping surrounding whitespace, and a field that is empty or all whitespace is nil as well -- a list that uses both conventions in one file (they all do) should not make an adapter check for both.
Defined Under Namespace
Classes: Row
Instance Attribute Summary collapse
- #col_sep ⇒ String readonly
-
#columns ⇒ Array<Symbol>?
readonly
nil where the file names its own columns -- see #headers?.
- #encoding ⇒ Encoding readonly
- #nulls ⇒ Array<String> readonly
- #quote_char ⇒ String readonly
Instance Method Summary collapse
-
#coerce(names, values) ⇒ Hash{Symbol => String, nil}
Zips a row's values against the column names.
-
#col_sep_name ⇒ String
What to call this format in an error message, so a complaint about OFAC's file says "CSV" rather than "delimited text".
- #csv_options ⇒ Hash{Symbol => T.untyped}
-
#headers? ⇒ Boolean
Whether the file names its own columns.
-
#initialize(columns: nil, null: nil, col_sep: ",", quote_char: '"', encoding: DEFAULT_ENCODING, liberal_parsing: true) ⇒ void
constructor
liberal_parsingis on by default because these files are published, not validated: an unescaped quote inside a company name is common enough in OFAC and UK OFSI data that failing the row is the wrong default. - #inspect ⇒ String
-
#read(payload) ⇒ Reader
A pass over one payload.
Constructor Details
#initialize(columns: nil, null: nil, col_sep: ",", quote_char: '"', encoding: DEFAULT_ENCODING, liberal_parsing: true) ⇒ void
liberal_parsing is on by default because these files are published,
not validated: an unescaped quote inside a company name is common
enough in OFAC and UK OFSI data that failing the row is the wrong
default. Turn it off for a source where a stray quote should be loud.
84 85 86 87 88 89 90 91 92 93 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 84 def initialize(columns: nil, null: nil, col_sep: ",", quote_char: '"', encoding: DEFAULT_ENCODING, liberal_parsing: true) @columns = T.let(columns!(columns), T.nilable(T::Array[Symbol])) @nulls = T.let(nulls!(null), T::Array[String]) @col_sep = T.let(col_sep.to_s, String) @quote_char = T.let(quote_char.to_s, String) @encoding = T.let(encoding!(encoding), Encoding) @liberal_parsing = T.let(liberal_parsing, T::Boolean) freeze end |
Instance Attribute Details
#col_sep ⇒ String (readonly)
65 66 67 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 65 def col_sep @col_sep end |
#columns ⇒ Array<Symbol>? (readonly)
nil where the file names its own columns -- see #headers?.
62 63 64 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 62 def columns @columns end |
#encoding ⇒ Encoding (readonly)
74 75 76 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 74 def encoding @encoding end |
#nulls ⇒ Array<String> (readonly)
71 72 73 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 71 def nulls @nulls end |
#quote_char ⇒ String (readonly)
68 69 70 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 68 def quote_char @quote_char end |
Instance Method Details
#coerce(names, values) ⇒ Hash{Symbol => String, nil}
Zips a row's values against the column names. Extra values are dropped and missing ones are nil; the Reader has already warned about both.
107 108 109 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 107 def coerce(names, values) names.each_with_index.to_h { |name, index| [name, value(values[index])] }.freeze end |
#col_sep_name ⇒ String
What to call this format in an error message, so a complaint about OFAC's file says "CSV" rather than "delimited text".
120 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 120 def col_sep_name = SEPARATOR_NAMES.fetch(col_sep, "delimited text") |
#csv_options ⇒ Hash{Symbol => T.untyped}
112 113 114 115 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 112 def { col_sep: col_sep, quote_char: quote_char, headers: false, liberal_parsing: @liberal_parsing, skip_blanks: true } end |
#headers? ⇒ Boolean
Whether the file names its own columns.
102 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 102 def headers? = columns.nil? |
#inspect ⇒ String
123 124 125 126 127 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 123 def inspect declared = columns shape = declared.nil? ? "headers from file" : "#{declared.size} columns" "#<#{self.class} #{col_sep_name} #{shape}#{" null=#{nulls.first.inspect}" if nulls.any?}>" end |
#read(payload) ⇒ Reader
A pass over one payload. Takes the bytes as a String, which is what Sources::Base hands #parse.
98 |
# File 'lib/active_sanction/parsers/delimited_table.rb', line 98 def read(payload) = Reader.new(table: self, payload: payload) |