lenex-parser 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +21 -0
  3. data/.yardopts +2 -0
  4. data/LICENSE +21 -0
  5. data/README.md +796 -0
  6. data/Rakefile +43 -0
  7. data/bin/console +8 -0
  8. data/bin/setup +5 -0
  9. data/lenex-parser.gemspec +35 -0
  10. data/lib/lenex/document/serializer.rb +191 -0
  11. data/lib/lenex/document.rb +163 -0
  12. data/lib/lenex/parser/objects/age_date.rb +53 -0
  13. data/lib/lenex/parser/objects/age_group.rb +86 -0
  14. data/lib/lenex/parser/objects/athlete.rb +93 -0
  15. data/lib/lenex/parser/objects/bank.rb +56 -0
  16. data/lib/lenex/parser/objects/club.rb +101 -0
  17. data/lib/lenex/parser/objects/constructor.rb +51 -0
  18. data/lib/lenex/parser/objects/contact.rb +55 -0
  19. data/lib/lenex/parser/objects/entry.rb +70 -0
  20. data/lib/lenex/parser/objects/entry_schedule.rb +40 -0
  21. data/lib/lenex/parser/objects/event.rb +114 -0
  22. data/lib/lenex/parser/objects/facility.rb +58 -0
  23. data/lib/lenex/parser/objects/fee.rb +54 -0
  24. data/lib/lenex/parser/objects/fee_schedule.rb +26 -0
  25. data/lib/lenex/parser/objects/handicap.rb +86 -0
  26. data/lib/lenex/parser/objects/heat.rb +58 -0
  27. data/lib/lenex/parser/objects/host_club.rb +34 -0
  28. data/lib/lenex/parser/objects/judge.rb +55 -0
  29. data/lib/lenex/parser/objects/lenex.rb +72 -0
  30. data/lib/lenex/parser/objects/meet.rb +175 -0
  31. data/lib/lenex/parser/objects/meet_info.rb +60 -0
  32. data/lib/lenex/parser/objects/official.rb +70 -0
  33. data/lib/lenex/parser/objects/organizer.rb +34 -0
  34. data/lib/lenex/parser/objects/point_table.rb +54 -0
  35. data/lib/lenex/parser/objects/pool.rb +44 -0
  36. data/lib/lenex/parser/objects/qualify.rb +55 -0
  37. data/lib/lenex/parser/objects/ranking.rb +54 -0
  38. data/lib/lenex/parser/objects/record.rb +107 -0
  39. data/lib/lenex/parser/objects/record_athlete.rb +92 -0
  40. data/lib/lenex/parser/objects/record_list.rb +106 -0
  41. data/lib/lenex/parser/objects/record_relay.rb +62 -0
  42. data/lib/lenex/parser/objects/record_relay_position.rb +62 -0
  43. data/lib/lenex/parser/objects/relay.rb +93 -0
  44. data/lib/lenex/parser/objects/relay_entry.rb +81 -0
  45. data/lib/lenex/parser/objects/relay_position.rb +74 -0
  46. data/lib/lenex/parser/objects/relay_result.rb +85 -0
  47. data/lib/lenex/parser/objects/result.rb +76 -0
  48. data/lib/lenex/parser/objects/session.rb +107 -0
  49. data/lib/lenex/parser/objects/split.rb +53 -0
  50. data/lib/lenex/parser/objects/swim_style.rb +58 -0
  51. data/lib/lenex/parser/objects/time_standard.rb +55 -0
  52. data/lib/lenex/parser/objects/time_standard_list.rb +98 -0
  53. data/lib/lenex/parser/objects/time_standard_ref.rb +63 -0
  54. data/lib/lenex/parser/objects.rb +52 -0
  55. data/lib/lenex/parser/sax/document_handler.rb +184 -0
  56. data/lib/lenex/parser/version.rb +8 -0
  57. data/lib/lenex/parser/zip_source.rb +111 -0
  58. data/lib/lenex/parser.rb +184 -0
  59. data/lib/lenex-parser.rb +16 -0
  60. metadata +132 -0
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a POINTTABLE element.
7
+ class PointTable
8
+ ATTRIBUTES = {
9
+ 'name' => { key: :name, required: true },
10
+ 'pointtableid' => { key: :point_table_id, required: false },
11
+ 'version' => { key: :version, required: true }
12
+ }.freeze
13
+
14
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
15
+ private_constant :ATTRIBUTE_KEYS
16
+
17
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
18
+
19
+ def initialize(**attributes)
20
+ ATTRIBUTES.each_value do |definition|
21
+ key = definition[:key]
22
+ instance_variable_set(:"@#{key}", attributes[key])
23
+ end
24
+ end
25
+
26
+ def self.from_xml(element)
27
+ raise ::Lenex::Parser::ParseError, 'POINTTABLE element is required' unless element
28
+
29
+ attributes = extract_attributes(element)
30
+
31
+ new(**attributes)
32
+ end
33
+
34
+ def self.extract_attributes(element)
35
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
36
+ value = element.attribute(attribute_name)&.value
37
+ ensure_required_attribute!(attribute_name, definition, value)
38
+ collected[definition[:key]] = value if value
39
+ end
40
+ end
41
+ private_class_method :extract_attributes
42
+
43
+ def self.ensure_required_attribute!(attribute_name, definition, value)
44
+ return unless definition[:required]
45
+ return unless value.nil? || value.strip.empty?
46
+
47
+ message = "POINTTABLE #{attribute_name} attribute is required"
48
+ raise ::Lenex::Parser::ParseError, message
49
+ end
50
+ private_class_method :ensure_required_attribute!
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a POOL element.
7
+ class Pool
8
+ ATTRIBUTES = {
9
+ 'lanemax' => :lane_max,
10
+ 'lanemin' => :lane_min,
11
+ 'temperature' => :temperature,
12
+ 'type' => :type
13
+ }.freeze
14
+
15
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.freeze
16
+ private_constant :ATTRIBUTE_KEYS
17
+
18
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
19
+
20
+ def initialize(**attributes)
21
+ ATTRIBUTES.each_value do |key|
22
+ instance_variable_set(:"@#{key}", attributes[key])
23
+ end
24
+ end
25
+
26
+ def self.from_xml(element)
27
+ raise ::Lenex::Parser::ParseError, 'POOL element is required' unless element
28
+
29
+ attributes = extract_attributes(element)
30
+
31
+ new(**attributes)
32
+ end
33
+
34
+ def self.extract_attributes(element)
35
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, key), collected|
36
+ value = element.attribute(attribute_name)&.value
37
+ collected[key] = value if value
38
+ end
39
+ end
40
+ private_class_method :extract_attributes
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a QUALIFY element.
7
+ class Qualify
8
+ ATTRIBUTES = {
9
+ 'conversion' => { key: :conversion, required: false },
10
+ 'from' => { key: :from, required: true },
11
+ 'percent' => { key: :percent, required: false },
12
+ 'until' => { key: :until, required: false }
13
+ }.freeze
14
+
15
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
16
+ private_constant :ATTRIBUTE_KEYS
17
+
18
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
19
+
20
+ def initialize(**attributes)
21
+ ATTRIBUTES.each_value do |definition|
22
+ key = definition[:key]
23
+ instance_variable_set(:"@#{key}", attributes[key])
24
+ end
25
+ end
26
+
27
+ def self.from_xml(element)
28
+ raise ::Lenex::Parser::ParseError, 'QUALIFY element is required' unless element
29
+
30
+ attributes = extract_attributes(element)
31
+
32
+ new(**attributes)
33
+ end
34
+
35
+ def self.extract_attributes(element)
36
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
37
+ value = element.attribute(attribute_name)&.value
38
+ ensure_required_attribute!(attribute_name, definition, value)
39
+ collected[definition[:key]] = value if value
40
+ end
41
+ end
42
+ private_class_method :extract_attributes
43
+
44
+ def self.ensure_required_attribute!(attribute_name, definition, value)
45
+ return unless definition[:required]
46
+ return unless value.nil? || value.strip.empty?
47
+
48
+ message = "QUALIFY #{attribute_name} attribute is required"
49
+ raise ::Lenex::Parser::ParseError, message
50
+ end
51
+ private_class_method :ensure_required_attribute!
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a RANKING element.
7
+ class Ranking
8
+ ATTRIBUTES = {
9
+ 'order' => { key: :order, required: false },
10
+ 'place' => { key: :place, required: true },
11
+ 'resultid' => { key: :result_id, required: true }
12
+ }.freeze
13
+
14
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
15
+ private_constant :ATTRIBUTE_KEYS
16
+
17
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
18
+
19
+ def initialize(**attributes)
20
+ ATTRIBUTES.each_value do |definition|
21
+ key = definition[:key]
22
+ instance_variable_set(:"@#{key}", attributes[key])
23
+ end
24
+ end
25
+
26
+ def self.from_xml(element)
27
+ raise ::Lenex::Parser::ParseError, 'RANKING element is required' unless element
28
+
29
+ attributes = extract_attributes(element)
30
+
31
+ new(**attributes)
32
+ end
33
+
34
+ def self.extract_attributes(element)
35
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
36
+ value = element.attribute(attribute_name)&.value
37
+ ensure_required_attribute!(attribute_name, definition, value)
38
+ collected[definition[:key]] = value if value
39
+ end
40
+ end
41
+ private_class_method :extract_attributes
42
+
43
+ def self.ensure_required_attribute!(attribute_name, definition, value)
44
+ return unless definition[:required]
45
+ return unless value.nil? || value.strip.empty?
46
+
47
+ message = "RANKING #{attribute_name} attribute is required"
48
+ raise ::Lenex::Parser::ParseError, message
49
+ end
50
+ private_class_method :ensure_required_attribute!
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a RECORD element.
7
+ class Record
8
+ ATTRIBUTES = {
9
+ 'swimtime' => { key: :swim_time, required: true },
10
+ 'status' => { key: :status, required: false },
11
+ 'comment' => { key: :comment, required: false }
12
+ }.freeze
13
+
14
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
15
+ private_constant :ATTRIBUTE_KEYS
16
+
17
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
18
+ attr_reader :meet_info, :swim_style, :athlete, :relay, :splits
19
+
20
+ def initialize(associations: {}, **attributes)
21
+ ATTRIBUTES.each_value do |definition|
22
+ key = definition[:key]
23
+ instance_variable_set(:"@#{key}", attributes[key])
24
+ end
25
+ @meet_info = associations[:meet_info]
26
+ @swim_style = associations[:swim_style]
27
+ @athlete = associations[:athlete]
28
+ @relay = associations[:relay]
29
+ @splits = Array(associations.fetch(:splits, []))
30
+ end
31
+
32
+ def self.from_xml(element)
33
+ raise ::Lenex::Parser::ParseError, 'RECORD element is required' unless element
34
+
35
+ attributes = extract_attributes(element)
36
+ ensure_required_attributes!(attributes)
37
+
38
+ new(**attributes, associations: associations_from(element))
39
+ end
40
+
41
+ def self.extract_attributes(element)
42
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
43
+ value = element.attribute(attribute_name)&.value
44
+ collected[definition[:key]] = value if value
45
+ end
46
+ end
47
+ private_class_method :extract_attributes
48
+
49
+ def self.ensure_required_attributes!(attributes)
50
+ value = attributes[:swim_time]
51
+ return unless value.nil? || value.strip.empty?
52
+
53
+ raise ::Lenex::Parser::ParseError, 'RECORD swimtime attribute is required'
54
+ end
55
+ private_class_method :ensure_required_attributes!
56
+
57
+ def self.meet_info_from(element)
58
+ return unless element
59
+
60
+ MeetInfo.from_xml(element)
61
+ end
62
+ private_class_method :meet_info_from
63
+
64
+ def self.swim_style_from(element)
65
+ raise ::Lenex::Parser::ParseError, 'RECORD SWIMSTYLE element is required' unless element
66
+
67
+ SwimStyle.from_xml(element)
68
+ end
69
+ private_class_method :swim_style_from
70
+
71
+ def self.athlete_from(element)
72
+ return unless element
73
+
74
+ RecordAthlete.from_xml(element)
75
+ end
76
+ private_class_method :athlete_from
77
+
78
+ def self.relay_from(element)
79
+ return unless element
80
+
81
+ RecordRelay.from_xml(element)
82
+ end
83
+ private_class_method :relay_from
84
+
85
+ def self.extract_splits(collection_element)
86
+ return [] unless collection_element
87
+
88
+ collection_element.xpath('SPLIT').map do |split_element|
89
+ Split.from_xml(split_element)
90
+ end
91
+ end
92
+ private_class_method :extract_splits
93
+
94
+ def self.associations_from(element)
95
+ {
96
+ meet_info: meet_info_from(element.at_xpath('MEETINFO')),
97
+ swim_style: swim_style_from(element.at_xpath('SWIMSTYLE')),
98
+ athlete: athlete_from(element.at_xpath('ATHLETE')),
99
+ relay: relay_from(element.at_xpath('RELAY')),
100
+ splits: extract_splits(element.at_xpath('SPLITS'))
101
+ }
102
+ end
103
+ private_class_method :associations_from
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,92 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing an ATHLETE element within a record.
7
+ class RecordAthlete
8
+ ATTRIBUTES = {
9
+ 'athleteid' => { key: :athlete_id, required: false },
10
+ 'birthdate' => { key: :birthdate, required: false },
11
+ 'firstname' => { key: :first_name, required: false },
12
+ 'firstname.en' => { key: :first_name_en, required: false },
13
+ 'gender' => { key: :gender, required: true },
14
+ 'lastname' => { key: :last_name, required: false },
15
+ 'lastname.en' => { key: :last_name_en, required: false },
16
+ 'level' => { key: :level, required: false },
17
+ 'license' => { key: :license, required: false },
18
+ 'license_dbs' => { key: :license_dbs, required: false },
19
+ 'license_dsv' => { key: :license_dsv, required: false },
20
+ 'license_ipc' => { key: :license_ipc, required: false },
21
+ 'nameprefix' => { key: :name_prefix, required: false },
22
+ 'nation' => { key: :nation, required: false },
23
+ 'passport' => { key: :passport, required: false },
24
+ 'status' => { key: :status, required: false },
25
+ 'swrid' => { key: :swrid, required: false }
26
+ }.freeze
27
+
28
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
29
+ private_constant :ATTRIBUTE_KEYS
30
+
31
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
32
+ attr_reader :club, :handicap
33
+
34
+ def initialize(club: nil, handicap: nil, **attributes)
35
+ ATTRIBUTES.each_value do |definition|
36
+ key = definition[:key]
37
+ instance_variable_set(:"@#{key}", attributes[key])
38
+ end
39
+ @club = club
40
+ @handicap = handicap
41
+ end
42
+
43
+ def self.from_xml(element)
44
+ raise ::Lenex::Parser::ParseError, 'ATHLETE element is required' unless element
45
+
46
+ attributes = extract_attributes(element)
47
+ ensure_required_attributes!(attributes)
48
+
49
+ club = club_from(element.at_xpath('CLUB'))
50
+ handicap = Handicap.from_xml(element.at_xpath('HANDICAP'))
51
+
52
+ new(**attributes, club:, handicap:)
53
+ end
54
+
55
+ def self.extract_attributes(element)
56
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
57
+ value = element.attribute(attribute_name)&.value
58
+ collected[definition[:key]] = value if value
59
+ end
60
+ end
61
+ private_class_method :extract_attributes
62
+
63
+ def self.ensure_required_attributes!(attributes)
64
+ ATTRIBUTES.each_value do |definition|
65
+ next unless definition[:required]
66
+
67
+ key = definition[:key]
68
+ value = attributes[key]
69
+ next unless value.nil? || value.strip.empty?
70
+
71
+ message = "ATHLETE #{ATTRIBUTE_NAME_FOR[key]} attribute is required"
72
+ raise ::Lenex::Parser::ParseError, message
73
+ end
74
+ end
75
+ private_class_method :ensure_required_attributes!
76
+
77
+ ATTRIBUTE_NAME_FOR = ATTRIBUTES.each_with_object({}) do |(attribute_name, definition),
78
+ mapping|
79
+ mapping[definition[:key]] = attribute_name
80
+ end.freeze
81
+ private_constant :ATTRIBUTE_NAME_FOR
82
+
83
+ def self.club_from(element)
84
+ return unless element
85
+
86
+ Club.from_xml(element)
87
+ end
88
+ private_class_method :club_from
89
+ end
90
+ end
91
+ end
92
+ end
@@ -0,0 +1,106 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a RECORDLIST element.
7
+ class RecordList
8
+ ATTRIBUTES = {
9
+ 'course' => { key: :course, required: true },
10
+ 'gender' => { key: :gender, required: true },
11
+ 'handicap' => { key: :handicap, required: false },
12
+ 'name' => { key: :name, required: true },
13
+ 'nation' => { key: :nation, required: false },
14
+ 'order' => { key: :order, required: false },
15
+ 'region' => { key: :region, required: false },
16
+ 'type' => { key: :type, required: false },
17
+ 'updated' => { key: :updated, required: false }
18
+ }.freeze
19
+
20
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
21
+ private_constant :ATTRIBUTE_KEYS
22
+
23
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
24
+ attr_reader :age_group, :records
25
+
26
+ def initialize(age_group: nil, records: [], **attributes)
27
+ ATTRIBUTES.each_value do |definition|
28
+ key = definition[:key]
29
+ instance_variable_set(:"@#{key}", attributes[key])
30
+ end
31
+ @age_group = age_group
32
+ @records = Array(records)
33
+ end
34
+
35
+ def self.from_xml(element)
36
+ raise ::Lenex::Parser::ParseError, 'RECORDLIST element is required' unless element
37
+
38
+ attributes = extract_attributes(element)
39
+ ensure_required_attributes!(attributes)
40
+
41
+ age_group = age_group_from(element.at_xpath('AGEGROUP'))
42
+ records = extract_records(element.at_xpath('RECORDS'))
43
+
44
+ new(**attributes, age_group:, records:)
45
+ end
46
+
47
+ def self.extract_attributes(element)
48
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
49
+ value = element.attribute(attribute_name)&.value
50
+ collected[definition[:key]] = value if value
51
+ end
52
+ end
53
+ private_class_method :extract_attributes
54
+
55
+ def self.ensure_required_attributes!(attributes)
56
+ %i[course gender name].each do |key|
57
+ value = attributes[key]
58
+ next unless value.nil? || value.strip.empty?
59
+
60
+ message = "RECORDLIST #{ATTRIBUTE_NAME_FOR[key]} attribute is required"
61
+ raise ::Lenex::Parser::ParseError, message
62
+ end
63
+
64
+ ensure_region_requires_nation!(attributes)
65
+ end
66
+ private_class_method :ensure_required_attributes!
67
+
68
+ def self.ensure_region_requires_nation!(attributes)
69
+ region = attributes[:region]
70
+ return unless region
71
+
72
+ nation = attributes[:nation]
73
+ return if nation && !nation.strip.empty?
74
+
75
+ message = 'RECORDLIST nation attribute is required when region attribute is present'
76
+ raise ::Lenex::Parser::ParseError, message
77
+ end
78
+ private_class_method :ensure_region_requires_nation!
79
+
80
+ ATTRIBUTE_NAME_FOR = ATTRIBUTES.each_with_object({}) do |(attribute_name, definition),
81
+ mapping|
82
+ mapping[definition[:key]] = attribute_name
83
+ end.freeze
84
+ private_constant :ATTRIBUTE_NAME_FOR
85
+
86
+ def self.age_group_from(element)
87
+ return unless element
88
+
89
+ AgeGroup.from_xml(element)
90
+ end
91
+ private_class_method :age_group_from
92
+
93
+ def self.extract_records(collection_element)
94
+ unless collection_element
95
+ raise ::Lenex::Parser::ParseError, 'RECORDLIST RECORDS element is required'
96
+ end
97
+
98
+ collection_element.xpath('RECORD').map do |record_element|
99
+ Record.from_xml(record_element)
100
+ end
101
+ end
102
+ private_class_method :extract_records
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a RELAY element within a record.
7
+ class RecordRelay
8
+ ATTRIBUTES = {
9
+ 'name' => :name
10
+ }.freeze
11
+
12
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.freeze
13
+ private_constant :ATTRIBUTE_KEYS
14
+
15
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
16
+ attr_reader :club, :relay_positions
17
+
18
+ def initialize(club: nil, relay_positions: [], **attributes)
19
+ ATTRIBUTES.each_value do |key|
20
+ instance_variable_set(:"@#{key}", attributes[key])
21
+ end
22
+ @club = club
23
+ @relay_positions = Array(relay_positions)
24
+ end
25
+
26
+ def self.from_xml(element)
27
+ raise ::Lenex::Parser::ParseError, 'RELAY element is required' unless element
28
+
29
+ attributes = extract_attributes(element)
30
+ club = club_from(element.at_xpath('CLUB'))
31
+ relay_positions = extract_relay_positions(element.at_xpath('RELAYPOSITIONS'))
32
+
33
+ new(**attributes, club:, relay_positions:)
34
+ end
35
+
36
+ def self.extract_attributes(element)
37
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, key), collected|
38
+ value = element.attribute(attribute_name)&.value
39
+ collected[key] = value if value
40
+ end
41
+ end
42
+ private_class_method :extract_attributes
43
+
44
+ def self.club_from(element)
45
+ return unless element
46
+
47
+ Club.from_xml(element)
48
+ end
49
+ private_class_method :club_from
50
+
51
+ def self.extract_relay_positions(collection_element)
52
+ return [] unless collection_element
53
+
54
+ collection_element.xpath('RELAYPOSITION').map do |position_element|
55
+ RecordRelayPosition.from_xml(position_element)
56
+ end
57
+ end
58
+ private_class_method :extract_relay_positions
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a RELAYPOSITION element within a record.
7
+ class RecordRelayPosition
8
+ ATTRIBUTES = {
9
+ 'number' => { key: :number, required: true },
10
+ 'reactiontime' => { key: :reaction_time, required: false },
11
+ 'status' => { key: :status, required: false }
12
+ }.freeze
13
+
14
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
15
+ private_constant :ATTRIBUTE_KEYS
16
+
17
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
18
+ attr_reader :athlete
19
+
20
+ def initialize(athlete:, **attributes)
21
+ ATTRIBUTES.each_value do |definition|
22
+ key = definition[:key]
23
+ instance_variable_set(:"@#{key}", attributes[key])
24
+ end
25
+ @athlete = athlete
26
+ end
27
+
28
+ def self.from_xml(element)
29
+ raise ::Lenex::Parser::ParseError, 'RELAYPOSITION element is required' unless element
30
+
31
+ attributes = extract_attributes(element)
32
+ ensure_required_attributes!(attributes)
33
+
34
+ athlete_element = element.at_xpath('ATHLETE')
35
+ if athlete_element.nil?
36
+ raise ::Lenex::Parser::ParseError, 'RELAYPOSITION ATHLETE element is required'
37
+ end
38
+
39
+ athlete = RecordAthlete.from_xml(athlete_element)
40
+
41
+ new(**attributes, athlete:)
42
+ end
43
+
44
+ def self.extract_attributes(element)
45
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
46
+ value = element.attribute(attribute_name)&.value
47
+ collected[definition[:key]] = value if value
48
+ end
49
+ end
50
+ private_class_method :extract_attributes
51
+
52
+ def self.ensure_required_attributes!(attributes)
53
+ value = attributes[:number]
54
+ return unless value.nil? || value.strip.empty?
55
+
56
+ raise ::Lenex::Parser::ParseError, 'RELAYPOSITION number attribute is required'
57
+ end
58
+ private_class_method :ensure_required_attributes!
59
+ end
60
+ end
61
+ end
62
+ end