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,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a HANDICAP element.
7
+ class Handicap
8
+ ATTRIBUTES = {
9
+ 'breast' => { key: :breast, required: true, missing_behavior: :warn },
10
+ 'breaststatus' => { key: :breast_status, required: false },
11
+ 'exception' => { key: :exception, required: false },
12
+ 'free' => { key: :free, required: false },
13
+ 'freestatus' => { key: :free_status, required: false },
14
+ 'medley' => { key: :medley, required: false },
15
+ 'medleystatus' => { key: :medley_status, required: false }
16
+ }.freeze
17
+
18
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
19
+ private_constant :ATTRIBUTE_KEYS
20
+
21
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
22
+
23
+ def initialize(**attributes)
24
+ ATTRIBUTES.each_value do |definition|
25
+ key = definition[:key]
26
+ instance_variable_set(:"@#{key}", attributes[key])
27
+ end
28
+ end
29
+
30
+ def self.from_xml(element)
31
+ return unless element
32
+
33
+ attributes = extract_attributes(element)
34
+ ensure_required_attributes!(attributes)
35
+
36
+ new(**attributes)
37
+ end
38
+
39
+ def self.extract_attributes(element)
40
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
41
+ value = element.attribute(attribute_name)&.value
42
+ collected[definition[:key]] = value if value && !value.strip.empty?
43
+ end
44
+ end
45
+ private_class_method :extract_attributes
46
+
47
+ def self.ensure_required_attributes!(attributes)
48
+ ATTRIBUTES.each_value do |definition|
49
+ next unless definition[:required]
50
+
51
+ key = definition[:key]
52
+ value = attributes[key]
53
+ next unless value.nil? || value.strip.empty?
54
+
55
+ attribute_name = ATTRIBUTE_NAME_FOR.fetch(key)
56
+ message = "HANDICAP #{attribute_name} attribute is required"
57
+ handle_missing_required_attribute(definition, message)
58
+ end
59
+ end
60
+ private_class_method :ensure_required_attributes!
61
+
62
+ def self.handle_missing_required_attribute(definition, message)
63
+ behavior = definition.fetch(:missing_behavior, :raise)
64
+ case behavior
65
+ when :warn
66
+ emit_warning(message)
67
+ else
68
+ raise ::Lenex::Parser::ParseError, message
69
+ end
70
+ end
71
+ private_class_method :handle_missing_required_attribute
72
+
73
+ def self.emit_warning(message)
74
+ warn(message)
75
+ end
76
+ private_class_method :emit_warning
77
+
78
+ ATTRIBUTE_NAME_FOR = ATTRIBUTES.each_with_object({}) do |(attribute_name, definition),
79
+ mapping|
80
+ mapping[definition[:key]] = attribute_name
81
+ end.freeze
82
+ private_constant :ATTRIBUTE_NAME_FOR
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a HEAT element.
7
+ class Heat
8
+ ATTRIBUTES = {
9
+ 'agegroupid' => { key: :age_group_id, required: false },
10
+ 'daytime' => { key: :daytime, required: false },
11
+ 'final' => { key: :final, required: false },
12
+ 'heatid' => { key: :heat_id, required: true },
13
+ 'number' => { key: :number, required: true },
14
+ 'order' => { key: :order, required: false },
15
+ 'status' => { key: :status, required: false }
16
+ }.freeze
17
+
18
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
19
+ private_constant :ATTRIBUTE_KEYS
20
+
21
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
22
+
23
+ def initialize(**attributes)
24
+ ATTRIBUTES.each_value do |definition|
25
+ key = definition[:key]
26
+ instance_variable_set(:"@#{key}", attributes[key])
27
+ end
28
+ end
29
+
30
+ def self.from_xml(element)
31
+ raise ::Lenex::Parser::ParseError, 'HEAT element is required' unless element
32
+
33
+ attributes = extract_attributes(element)
34
+
35
+ new(**attributes)
36
+ end
37
+
38
+ def self.extract_attributes(element)
39
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
40
+ value = element.attribute(attribute_name)&.value
41
+ ensure_required_attribute!(attribute_name, definition, value)
42
+ collected[definition[:key]] = value if value
43
+ end
44
+ end
45
+ private_class_method :extract_attributes
46
+
47
+ def self.ensure_required_attribute!(attribute_name, definition, value)
48
+ return unless definition[:required]
49
+ return unless value.nil? || value.strip.empty?
50
+
51
+ message = "HEAT #{attribute_name} attribute is required"
52
+ raise ::Lenex::Parser::ParseError, message
53
+ end
54
+ private_class_method :ensure_required_attribute!
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing host club metadata on a MEET element.
7
+ class HostClub
8
+ attr_reader :name, :url
9
+
10
+ def initialize(name:, url: nil)
11
+ @name = name
12
+ @url = url
13
+ end
14
+
15
+ def self.from_xml(meet_element)
16
+ name = attribute_value(meet_element, 'hostclub')
17
+ url = attribute_value(meet_element, 'hostclub.url')
18
+
19
+ return unless name || url
20
+
21
+ new(name:, url:)
22
+ end
23
+
24
+ def self.attribute_value(element, attribute_name)
25
+ value = element.attribute(attribute_name)&.value
26
+ return if value.nil? || value.strip.empty?
27
+
28
+ value
29
+ end
30
+ private_class_method :attribute_value
31
+ end
32
+ end
33
+ end
34
+ 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 JUDGE element.
7
+ class Judge
8
+ ATTRIBUTES = {
9
+ 'number' => { key: :number, required: false },
10
+ 'officialid' => { key: :official_id, required: true },
11
+ 'remarks' => { key: :remarks, required: false },
12
+ 'role' => { key: :role, 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, 'JUDGE 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 = "JUDGE #{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,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing the LENEX root element.
7
+ class Lenex
8
+ attr_reader :version, :revision, :constructor, :meets, :record_lists, :time_standard_lists
9
+
10
+ def initialize(version:, revision:, constructor:, collections: {})
11
+ @version = version
12
+ @revision = revision
13
+ @constructor = constructor
14
+ @meets = Array(collections.fetch(:meets, []))
15
+ @record_lists = Array(collections.fetch(:record_lists, []))
16
+ @time_standard_lists = Array(collections.fetch(:time_standard_lists, []))
17
+ end
18
+
19
+ def self.from_xml(element)
20
+ version = version_from(element)
21
+ constructor = Constructor.from_xml(element.at_xpath('CONSTRUCTOR'))
22
+ revision = element.attribute('revision')&.value
23
+ collections = build_collections(element)
24
+
25
+ new(version:, revision:, constructor:, collections:)
26
+ end
27
+
28
+ def self.version_from(element)
29
+ version = element.attribute('version')&.value
30
+ return version if version && !version.strip.empty?
31
+
32
+ raise ::Lenex::Parser::ParseError, 'LENEX version attribute is required'
33
+ end
34
+ private_class_method :version_from
35
+
36
+ def self.build_collections(element)
37
+ {
38
+ meets: extract_meets(element.at_xpath('MEETS')),
39
+ record_lists: extract_record_lists(element.at_xpath('RECORDLISTS')),
40
+ time_standard_lists: extract_time_standard_lists(element.at_xpath('TIMESTANDARDLISTS'))
41
+ }
42
+ end
43
+ private_class_method :build_collections
44
+
45
+ def self.extract_meets(collection_element)
46
+ return [] unless collection_element
47
+
48
+ collection_element.xpath('MEET').map { |meet_element| Meet.from_xml(meet_element) }
49
+ end
50
+ private_class_method :extract_meets
51
+
52
+ def self.extract_record_lists(collection_element)
53
+ return [] unless collection_element
54
+
55
+ collection_element.xpath('RECORDLIST').map do |record_list_element|
56
+ RecordList.from_xml(record_list_element)
57
+ end
58
+ end
59
+ private_class_method :extract_record_lists
60
+
61
+ def self.extract_time_standard_lists(collection_element)
62
+ return [] unless collection_element
63
+
64
+ collection_element.xpath('TIMESTANDARDLIST').map do |time_standard_list_element|
65
+ TimeStandardList.from_xml(time_standard_list_element)
66
+ end
67
+ end
68
+ private_class_method :extract_time_standard_lists
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,175 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Helper namespace that extracts MEET associations from XML nodes.
7
+ module MeetAssociations
8
+ module_function
9
+
10
+ def build(element)
11
+ metadata_from(element).merge(collections_from(element))
12
+ end
13
+
14
+ def metadata_from(element)
15
+ core_metadata(element).merge(optional_metadata(element))
16
+ end
17
+ private_class_method :metadata_from
18
+
19
+ def core_metadata(element)
20
+ {
21
+ contact: contact_from(element.at_xpath('CONTACT')),
22
+ age_date: association_from(element.at_xpath('AGEDATE'), AgeDate),
23
+ bank: association_from(element.at_xpath('BANK'), Bank),
24
+ facility: association_from(element.at_xpath('FACILITY'), Facility),
25
+ point_table: association_from(element.at_xpath('POINTTABLE'), PointTable),
26
+ qualify: association_from(element.at_xpath('QUALIFY'), Qualify),
27
+ pool: association_from(element.at_xpath('POOL'), Pool)
28
+ }
29
+ end
30
+ private_class_method :core_metadata
31
+
32
+ def optional_metadata(element)
33
+ {
34
+ fee_schedule: FeeSchedule.from_xml(element.at_xpath('FEES')),
35
+ host_club: HostClub.from_xml(element),
36
+ organizer: Organizer.from_xml(element),
37
+ entry_schedule: EntrySchedule.from_xml(element)
38
+ }
39
+ end
40
+ private_class_method :optional_metadata
41
+
42
+ def collections_from(element)
43
+ {
44
+ clubs: extract_clubs(element.at_xpath('CLUBS')),
45
+ sessions: extract_sessions(element.at_xpath('SESSIONS'))
46
+ }
47
+ end
48
+ private_class_method :collections_from
49
+
50
+ def association_from(element, klass)
51
+ return unless element
52
+
53
+ klass.from_xml(element)
54
+ end
55
+ private_class_method :association_from
56
+
57
+ def contact_from(element)
58
+ return unless element
59
+
60
+ Contact.from_xml(element)
61
+ end
62
+ private_class_method :contact_from
63
+
64
+ def extract_clubs(collection_element)
65
+ return [] unless collection_element
66
+
67
+ collection_element.xpath('CLUB').map { |club_element| Club.from_xml(club_element) }
68
+ end
69
+ private_class_method :extract_clubs
70
+
71
+ def extract_sessions(collection_element)
72
+ unless collection_element
73
+ raise ::Lenex::Parser::ParseError, 'MEET SESSIONS element is required'
74
+ end
75
+
76
+ session_elements = collection_element.xpath('SESSION')
77
+ if session_elements.empty?
78
+ raise ::Lenex::Parser::ParseError, 'MEET SESSIONS element is required'
79
+ end
80
+
81
+ session_elements.map { |session_element| Session.from_xml(session_element) }
82
+ end
83
+ private_class_method :extract_sessions
84
+ end
85
+
86
+ # Value object representing a MEET element.
87
+ class Meet
88
+ ATTRIBUTES = {
89
+ 'name' => { key: :name, required: true },
90
+ 'name.en' => { key: :name_en, required: false },
91
+ 'city' => { key: :city, required: true },
92
+ 'city.en' => { key: :city_en, required: false },
93
+ 'nation' => { key: :nation, required: true },
94
+ 'course' => { key: :course, required: false },
95
+ 'number' => { key: :number, required: false },
96
+ 'reservecount' => { key: :reserve_count, required: false },
97
+ 'startmethod' => { key: :start_method, required: false },
98
+ 'timing' => { key: :timing, required: false },
99
+ 'touchpadmode' => { key: :touchpad_mode, required: false },
100
+ 'type' => { key: :type, required: false },
101
+ 'entrytype' => { key: :entry_type, required: false },
102
+ 'maxentriesathlete' => { key: :max_entries_athlete, required: false },
103
+ 'maxentriesrelay' => { key: :max_entries_relay, required: false },
104
+ 'altitude' => { key: :altitude, required: false },
105
+ 'swrid' => { key: :swrid, required: false },
106
+ 'result.url' => { key: :result_url, required: false }
107
+ }.freeze
108
+
109
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
110
+ private_constant :ATTRIBUTE_KEYS
111
+
112
+ ASSOCIATION_DEFAULTS = {
113
+ contact: nil,
114
+ clubs: [],
115
+ sessions: [],
116
+ age_date: nil,
117
+ bank: nil,
118
+ facility: nil,
119
+ point_table: nil,
120
+ qualify: nil,
121
+ pool: nil,
122
+ fee_schedule: nil,
123
+ host_club: nil,
124
+ organizer: nil,
125
+ entry_schedule: nil
126
+ }.freeze
127
+
128
+ ASSOCIATION_KEYS = ASSOCIATION_DEFAULTS.keys.freeze
129
+ private_constant :ASSOCIATION_KEYS
130
+
131
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
132
+ ASSOCIATION_KEYS.each { |attribute| attr_reader attribute }
133
+
134
+ def initialize(**attributes)
135
+ ATTRIBUTES.each_value do |definition|
136
+ key = definition[:key]
137
+ instance_variable_set(:"@#{key}", attributes[key])
138
+ end
139
+ ASSOCIATION_DEFAULTS.each do |key, default|
140
+ value = attributes.fetch(key, default)
141
+ value = Array(value) if %i[clubs sessions].include?(key)
142
+ instance_variable_set(:"@#{key}", value)
143
+ end
144
+ end
145
+
146
+ def self.from_xml(element)
147
+ raise ::Lenex::Parser::ParseError, 'MEET element is required' unless element
148
+
149
+ attributes = extract_attributes(element)
150
+ associations = MeetAssociations.build(element)
151
+
152
+ new(**attributes, **associations)
153
+ end
154
+
155
+ def self.extract_attributes(element)
156
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
157
+ value = element.attribute(attribute_name)&.value
158
+ ensure_required_attribute!(attribute_name, definition, value)
159
+ collected[definition[:key]] = value if value
160
+ end
161
+ end
162
+ private_class_method :extract_attributes
163
+
164
+ def self.ensure_required_attribute!(attribute_name, definition, value)
165
+ return unless definition[:required]
166
+ return unless value.nil? || value.strip.empty?
167
+
168
+ message = "MEET #{attribute_name} attribute is required"
169
+ raise ::Lenex::Parser::ParseError, message
170
+ end
171
+ private_class_method :ensure_required_attribute!
172
+ end
173
+ end
174
+ end
175
+ end
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a MEETINFO element.
7
+ class MeetInfo
8
+ ATTRIBUTES = {
9
+ 'approved' => :approved,
10
+ 'city' => :city,
11
+ 'course' => :course,
12
+ 'date' => :date,
13
+ 'daytime' => :daytime,
14
+ 'name' => :name,
15
+ 'nation' => :nation,
16
+ 'qualificationtime' => :qualification_time,
17
+ 'state' => :state,
18
+ 'timing' => :timing
19
+ }.freeze
20
+
21
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.freeze
22
+ private_constant :ATTRIBUTE_KEYS
23
+
24
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
25
+ attr_reader :pool
26
+
27
+ def initialize(pool: nil, **attributes)
28
+ ATTRIBUTES.each_value do |key|
29
+ instance_variable_set(:"@#{key}", attributes[key])
30
+ end
31
+ @pool = pool
32
+ end
33
+
34
+ def self.from_xml(element)
35
+ raise ::Lenex::Parser::ParseError, 'MEETINFO element is required' unless element
36
+
37
+ attributes = extract_attributes(element)
38
+ pool = pool_from(element.at_xpath('POOL'))
39
+
40
+ new(**attributes, pool:)
41
+ end
42
+
43
+ def self.extract_attributes(element)
44
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, key), collected|
45
+ value = element.attribute(attribute_name)&.value
46
+ collected[key] = value if value
47
+ end
48
+ end
49
+ private_class_method :extract_attributes
50
+
51
+ def self.pool_from(element)
52
+ return unless element
53
+
54
+ Pool.from_xml(element)
55
+ end
56
+ private_class_method :pool_from
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing an OFFICIAL element.
7
+ class Official
8
+ ATTRIBUTES = {
9
+ 'firstname' => { key: :first_name, required: true },
10
+ 'gender' => { key: :gender, required: false },
11
+ 'grade' => { key: :grade, required: false },
12
+ 'lastname' => { key: :last_name, required: true },
13
+ 'license' => { key: :license, required: false },
14
+ 'nameprefix' => { key: :name_prefix, required: false },
15
+ 'nation' => { key: :nation, required: false },
16
+ 'officialid' => { key: :official_id, required: true },
17
+ 'passport' => { key: :passport, 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 :contact
25
+
26
+ def initialize(contact: nil, **attributes)
27
+ ATTRIBUTES.each_value do |definition|
28
+ key = definition[:key]
29
+ instance_variable_set(:"@#{key}", attributes[key])
30
+ end
31
+ @contact = contact
32
+ end
33
+
34
+ def self.from_xml(element)
35
+ raise ::Lenex::Parser::ParseError, 'OFFICIAL element is required' unless element
36
+
37
+ attributes = extract_attributes(element)
38
+ contact = contact_from(element.at_xpath('CONTACT'))
39
+
40
+ new(**attributes, contact:)
41
+ end
42
+
43
+ def self.extract_attributes(element)
44
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
45
+ value = element.attribute(attribute_name)&.value
46
+ ensure_required_attribute!(attribute_name, definition, value)
47
+ collected[definition[:key]] = value if value
48
+ end
49
+ end
50
+ private_class_method :extract_attributes
51
+
52
+ def self.ensure_required_attribute!(attribute_name, definition, value)
53
+ return unless definition[:required]
54
+ return unless value.nil? || value.strip.empty?
55
+
56
+ message = "OFFICIAL #{attribute_name} attribute is required"
57
+ raise ::Lenex::Parser::ParseError, message
58
+ end
59
+ private_class_method :ensure_required_attribute!
60
+
61
+ def self.contact_from(element)
62
+ return unless element
63
+
64
+ Contact.from_xml(element)
65
+ end
66
+ private_class_method :contact_from
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing organizer metadata on a MEET element.
7
+ class Organizer
8
+ attr_reader :name, :url
9
+
10
+ def initialize(name:, url: nil)
11
+ @name = name
12
+ @url = url
13
+ end
14
+
15
+ def self.from_xml(meet_element)
16
+ name = attribute_value(meet_element, 'organizer')
17
+ url = attribute_value(meet_element, 'organizer.url')
18
+
19
+ return unless name || url
20
+
21
+ new(name:, url:)
22
+ end
23
+
24
+ def self.attribute_value(element, attribute_name)
25
+ value = element.attribute(attribute_name)&.value
26
+ return if value.nil? || value.strip.empty?
27
+
28
+ value
29
+ end
30
+ private_class_method :attribute_value
31
+ end
32
+ end
33
+ end
34
+ end