Class: ActiveSanction::Parsers::XmlRecords::Record

Inherits:
Object
  • Object
show all
Extended by:
T::Sig
Defined in:
lib/active_sanction/parsers/xml_records/record.rb

Overview

One record element and everything under it: the UN's <INDIVIDUAL>, Canada's <record>. Fields are read by path relative to the record, and a nested element is itself a Record, which is what lets an adapter walk repeated children without knowing how the file was parsed.

record.name                       # => "INDIVIDUAL"
record["DATAID"]                  # => "6907993"
record["INDIVIDUAL_DATE_OF_BIRTH/YEAR"]
record.values("NATIONALITY/VALUE")     # => ["Chad", "Sudan"]
record.nodes("INDIVIDUAL_ALIAS")       # => [Record, Record]
record["@dateGenerated"]               # an attribute, XPath-style

Absent, empty, and blank are one answer

An element that is missing, self-closing, or holds only whitespace all read as nil. That is not laziness about the difference; it is the only reading that survives the UN, which files placeholder aliases as <INDIVIDUAL_ALIAS><QUALITY/><ALIAS_NAME/></INDIVIDUAL_ALIAS> and means nothing at all by them. An adapter that had to distinguish the three would produce blank-valued Names for every one of those placeholders.

Why #[] does not raise the way a CSV Row does

DelimitedTable::Row raises on a column its table never declared, since a table's shape is fixed and an unknown name there is a typo. XML has no such shape: one <INDIVIDUAL> carries elements the next one omits, so an absent path is ordinary and #[] answers nil. #fetch is there for the field an adapter considers mandatory, and it names the record and what the record does carry when the field is missing.

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(table:, name:, attributes: {}, text: nil, children: [], line: nil) ⇒ void

Parameters:

  • table (XmlRecords)
  • name (T.untyped)
  • attributes (Hash{String => T.untyped}) (defaults to: {})
  • text (T.untyped) (defaults to: nil)
  • children (Array<Record>) (defaults to: [])
  • line (Integer, nil) (defaults to: nil)


69
70
71
72
73
74
75
76
77
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 69

def initialize(table:, name:, attributes: {}, text: nil, children: [], line: nil)
  @table = T.let(table, XmlRecords)
  @name = T.let(-name.to_s, String)
  @attributes = T.let(attributes.freeze, T::Hash[String, T.untyped])
  @raw_text = T.let(text, T.untyped)
  @children = T.let(children.freeze, T::Array[Record])
  @line = T.let(line, T.nilable(Integer))
  freeze
end

Instance Attribute Details

#attributes ⇒ Hash{String => T.untyped} (readonly)

Returns:

  • (Hash{String => T.untyped})


56
57
58
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 56

def attributes
  @attributes
end

#children ⇒ Array<Record> (readonly)

Returns:



59
60
61
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 59

def children
  @children
end

#line ⇒ Integer? (readonly)

nil where the backend reports no position.

Returns:

  • (Integer, nil)


63
64
65
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 63

def line
  @line
end

#name ⇒ String (readonly)

The element's own name, with any namespace prefix already removed by the backend -- see Backends.local_name.

Returns:

  • (String)


53
54
55
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 53

def name
  @name
end

#table ⇒ XmlRecords (readonly, protected)

Returns:



146
147
148
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 146

def table
  @table
end

Instance Method Details

#[](path) ⇒ String?

The first value at path, or nil if nothing is there.

Parameters:

  • path (T.untyped)

Returns:

  • (String, nil)


86
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 86

def [](path) = values(path).first

#attribute(key) ⇒ String?

Parameters:

  • key (T.untyped)

Returns:

  • (String, nil)


111
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 111

def attribute(key) = table.value(attributes[key.to_s])

#children_named(wanted) ⇒ Array<Record> (protected)

Parameters:

  • wanted (String)

Returns:



149
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 149

def children_named(wanted) = children.select { |child| child.name == wanted }

#fetch(path, default = UNSET) ⇒ T.untyped

For a field the adapter treats as mandatory. Raises rather than letting a renamed element arrive downstream as a nil nobody notices.

Parameters:

  • path (T.untyped)
  • default (T.untyped) (defaults to: UNSET)

Returns:

  • (T.untyped)

Raises:



116
117
118
119
120
121
122
123
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 116

def fetch(path, default = UNSET)
  value = self[path]
  return value unless value.nil?
  return default unless default.equal?(UNSET)

  raise MissingKey, "no value at #{path.inspect} in <#{name}>#{" on line #{line}" if line}. " \
                    "It carries: #{present.join(", ")}"
end

#inspect ⇒ String

Returns:

  • (String)


141
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 141

def inspect = "#<#{self.class} <#{name}>#{" line=#{line}" if line} #{present.join(" ")}>"

#nodes(path) ⇒ Array<Record>

The elements at path, as Records, whether or not they hold text -- an adapter reading <INDIVIDUAL_ADDRESS> wants the node, not a value.

Parameters:

  • path (T.untyped)

Returns:

Raises:



103
104
105
106
107
108
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 103

def nodes(path)
  steps, attribute = split(path)
  raise InvalidArgument, "#nodes reads elements, not the attribute #{path.inspect}" if attribute

  descend(steps)
end

#null?(path) ⇒ Boolean

Parameters:

  • path (T.untyped)

Returns:

  • (Boolean)


126
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 126

def null?(path) = self[path].nil?

#present ⇒ Array<String>

The child element names that actually carry something, which is what a #fetch failure has to print and what makes an unfamiliar list explorable from a console.

Returns:

  • (Array<String>)


132
133
134
135
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 132

def present
  names = children.select { |child| !child.text.nil? || child.children.any? }.map(&:name)
  names.uniq
end

#text ⇒ String?

This element's own text, with blanks and any declared null sentinel resolved to nil. Text belonging to child elements is not included.

Returns:

  • (String, nil)


82
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 82

def text = table.value(@raw_text)

#to_s ⇒ String

Returns:

  • (String)


138
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 138

def to_s = text.to_s

#values(path) ⇒ Array<String>

Every value at path, in document order, with blanks dropped. The answer to a repeated element: the UN files each nationality as its own <NATIONALITY><VALUE>.

Parameters:

  • path (T.untyped)

Returns:

  • (Array<String>)


92
93
94
95
96
97
98
# File 'lib/active_sanction/parsers/xml_records/record.rb', line 92

def values(path)
  steps, attribute = split(path)
  nodes = descend(steps)
  return nodes.filter_map { |node| node.attribute(attribute) } if attribute

  nodes.filter_map(&:text)
end