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,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a BANK element.
7
+ class Bank
8
+ ATTRIBUTES = {
9
+ 'accountholder' => { key: :account_holder, required: false },
10
+ 'bic' => { key: :bic, required: false },
11
+ 'iban' => { key: :iban, required: true },
12
+ 'name' => { key: :name, required: false },
13
+ 'note' => { key: :note, required: false }
14
+ }.freeze
15
+
16
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
17
+ private_constant :ATTRIBUTE_KEYS
18
+
19
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
20
+
21
+ def initialize(**attributes)
22
+ ATTRIBUTES.each_value do |definition|
23
+ key = definition[:key]
24
+ instance_variable_set(:"@#{key}", attributes[key])
25
+ end
26
+ end
27
+
28
+ def self.from_xml(element)
29
+ raise ::Lenex::Parser::ParseError, 'BANK element is required' unless element
30
+
31
+ attributes = extract_attributes(element)
32
+
33
+ new(**attributes)
34
+ end
35
+
36
+ def self.extract_attributes(element)
37
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
38
+ value = element.attribute(attribute_name)&.value
39
+ ensure_required_attribute!(attribute_name, definition, value)
40
+ collected[definition[:key]] = value if value
41
+ end
42
+ end
43
+ private_class_method :extract_attributes
44
+
45
+ def self.ensure_required_attribute!(attribute_name, definition, value)
46
+ return unless definition[:required]
47
+ return unless value.nil? || value.strip.empty?
48
+
49
+ message = "BANK #{attribute_name} attribute is required"
50
+ raise ::Lenex::Parser::ParseError, message
51
+ end
52
+ private_class_method :ensure_required_attribute!
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,101 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a CLUB element.
7
+ class Club
8
+ ATTRIBUTES = {
9
+ 'name' => { key: :name, required: true },
10
+ 'name.en' => { key: :name_en, required: false },
11
+ 'shortname' => { key: :shortname, required: false },
12
+ 'shortname.en' => { key: :shortname_en, required: false },
13
+ 'code' => { key: :code, required: false },
14
+ 'nation' => { key: :nation, required: false },
15
+ 'number' => { key: :number, required: false },
16
+ 'region' => { key: :region, required: false },
17
+ 'swrid' => { key: :swrid, required: false },
18
+ 'type' => { key: :type, required: false }
19
+ }.freeze
20
+
21
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
22
+ private_constant :ATTRIBUTE_KEYS
23
+
24
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
25
+ attr_reader :contact, :athletes, :officials, :relays
26
+
27
+ def initialize(contact: nil, athletes: [], officials: [], relays: [], **attributes)
28
+ ATTRIBUTES.each_value do |definition|
29
+ key = definition[:key]
30
+ instance_variable_set(:"@#{key}", attributes[key])
31
+ end
32
+ @contact = contact
33
+ @athletes = Array(athletes)
34
+ @officials = Array(officials)
35
+ @relays = Array(relays)
36
+ end
37
+
38
+ def self.from_xml(element)
39
+ raise ::Lenex::Parser::ParseError, 'CLUB element is required' unless element
40
+
41
+ attributes = extract_attributes(element)
42
+ ensure_required_attributes!(attributes)
43
+
44
+ contact_element = element.at_xpath('CONTACT')
45
+ contact = contact_element ? Contact.from_xml(contact_element) : nil
46
+ athletes = extract_athletes(element.at_xpath('ATHLETES'))
47
+ officials = extract_officials(element.at_xpath('OFFICIALS'))
48
+ relays = extract_relays(element.at_xpath('RELAYS'))
49
+
50
+ new(**attributes, contact:, athletes:, officials:, relays:)
51
+ end
52
+
53
+ def self.extract_attributes(element)
54
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
55
+ value = element.attribute(attribute_name)&.value
56
+ collected[definition[:key]] = value if value
57
+ end
58
+ end
59
+ private_class_method :extract_attributes
60
+
61
+ def self.ensure_required_attributes!(attributes)
62
+ name = attributes[:name]
63
+ type = attributes[:type]
64
+
65
+ return unless name.nil? || name.strip.empty?
66
+ return if type == 'UNATTACHED'
67
+
68
+ raise ::Lenex::Parser::ParseError, 'CLUB name attribute is required'
69
+ end
70
+ private_class_method :ensure_required_attributes!
71
+
72
+ def self.extract_athletes(collection_element)
73
+ return [] unless collection_element
74
+
75
+ collection_element.xpath('ATHLETE').map do |athlete_element|
76
+ Athlete.from_xml(athlete_element)
77
+ end
78
+ end
79
+ private_class_method :extract_athletes
80
+
81
+ def self.extract_officials(collection_element)
82
+ return [] unless collection_element
83
+
84
+ collection_element.xpath('OFFICIAL').map do |official_element|
85
+ Official.from_xml(official_element)
86
+ end
87
+ end
88
+ private_class_method :extract_officials
89
+
90
+ def self.extract_relays(collection_element)
91
+ return [] unless collection_element
92
+
93
+ collection_element.xpath('RELAY').map do |relay_element|
94
+ Relay.from_xml(relay_element)
95
+ end
96
+ end
97
+ private_class_method :extract_relays
98
+ end
99
+ end
100
+ end
101
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a CONSTRUCTOR element.
7
+ class Constructor
8
+ ATTRIBUTES = {
9
+ 'name' => :name,
10
+ 'registration' => :registration,
11
+ 'version' => :version
12
+ }.freeze
13
+
14
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.freeze
15
+ private_constant :ATTRIBUTE_KEYS
16
+
17
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
18
+ attr_reader :contact
19
+
20
+ def initialize(name:, registration:, version:, contact:)
21
+ @name = name
22
+ @registration = registration
23
+ @version = version
24
+ @contact = contact
25
+ end
26
+
27
+ def self.from_xml(element)
28
+ raise ::Lenex::Parser::ParseError, 'CONSTRUCTOR element is required' unless element
29
+
30
+ data = attributes_from(element)
31
+ contact = Contact.from_xml(element.at_xpath('CONTACT'), email_required: true)
32
+
33
+ new(**data, contact:)
34
+ end
35
+
36
+ def self.attributes_from(element)
37
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, key), attributes|
38
+ value = element.attribute(attribute_name)&.value
39
+ if value.nil? || value.strip.empty?
40
+ message = "CONSTRUCTOR #{attribute_name} attribute is required"
41
+ raise ::Lenex::Parser::ParseError, message
42
+ end
43
+
44
+ attributes[key] = value
45
+ end
46
+ end
47
+ private_class_method :attributes_from
48
+ end
49
+ end
50
+ end
51
+ 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 CONTACT element.
7
+ class Contact
8
+ ATTRIBUTES = {
9
+ 'name' => :name,
10
+ 'street' => :street,
11
+ 'street2' => :street2,
12
+ 'zip' => :zip,
13
+ 'city' => :city,
14
+ 'state' => :state,
15
+ 'country' => :country,
16
+ 'phone' => :phone,
17
+ 'mobile' => :mobile,
18
+ 'fax' => :fax,
19
+ 'email' => :email,
20
+ 'internet' => :internet
21
+ }.freeze
22
+
23
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.freeze
24
+ private_constant :ATTRIBUTE_KEYS
25
+
26
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
27
+
28
+ def initialize(**attributes)
29
+ ATTRIBUTES.each_value do |key|
30
+ instance_variable_set(:"@#{key}", attributes[key])
31
+ end
32
+ end
33
+
34
+ def self.from_xml(element, email_required: false)
35
+ raise ::Lenex::Parser::ParseError, 'CONTACT element is required' unless element
36
+
37
+ data = extract_attributes(element)
38
+ if email_required && (data[:email].nil? || data[:email].strip.empty?)
39
+ raise ::Lenex::Parser::ParseError, 'CONTACT email attribute is required'
40
+ end
41
+
42
+ new(**data)
43
+ end
44
+
45
+ def self.extract_attributes(element)
46
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, key), attributes|
47
+ value = element.attribute(attribute_name)&.value
48
+ attributes[key] = value if value
49
+ end
50
+ end
51
+ private_class_method :extract_attributes
52
+ end
53
+ end
54
+ end
55
+ 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 ENTRY element.
7
+ class Entry
8
+ ATTRIBUTES = {
9
+ 'agegroupid' => { key: :age_group_id, required: false },
10
+ 'entrycourse' => { key: :entry_course, required: false },
11
+ 'entrydistance' => { key: :entry_distance, required: false },
12
+ 'entrytime' => { key: :entry_time, required: false },
13
+ 'eventid' => { key: :event_id, required: true },
14
+ 'handicap' => { key: :handicap, required: false },
15
+ 'heatid' => { key: :heat_id, required: false },
16
+ 'lane' => { key: :lane, required: false },
17
+ 'status' => { key: :status, 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 :meet_info
25
+
26
+ def initialize(meet_info: nil, **attributes)
27
+ ATTRIBUTES.each_value do |definition|
28
+ key = definition[:key]
29
+ instance_variable_set(:"@#{key}", attributes[key])
30
+ end
31
+ @meet_info = meet_info
32
+ end
33
+
34
+ def self.from_xml(element)
35
+ raise ::Lenex::Parser::ParseError, 'ENTRY element is required' unless element
36
+
37
+ attributes = extract_attributes(element)
38
+ meet_info = meet_info_from(element.at_xpath('MEETINFO'))
39
+
40
+ new(**attributes, meet_info:)
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 = "ENTRY #{attribute_name} attribute is required"
57
+ raise ::Lenex::Parser::ParseError, message
58
+ end
59
+ private_class_method :ensure_required_attribute!
60
+
61
+ def self.meet_info_from(element)
62
+ return unless element
63
+
64
+ MeetInfo.from_xml(element)
65
+ end
66
+ private_class_method :meet_info_from
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object capturing entry schedule metadata on a MEET element.
7
+ class EntrySchedule
8
+ attr_reader :entry_start_date, :withdraw_until, :deadline_date, :deadline_time
9
+
10
+ def initialize(entry_start_date:, withdraw_until:, deadline_date:, deadline_time:)
11
+ @entry_start_date = entry_start_date
12
+ @withdraw_until = withdraw_until
13
+ @deadline_date = deadline_date
14
+ @deadline_time = deadline_time
15
+ end
16
+
17
+ def self.from_xml(meet_element)
18
+ attributes = {
19
+ entry_start_date: attribute_value(meet_element, 'entrystartdate'),
20
+ withdraw_until: attribute_value(meet_element, 'withdrawuntil'),
21
+ deadline_date: attribute_value(meet_element, 'deadline'),
22
+ deadline_time: attribute_value(meet_element, 'deadlinetime')
23
+ }
24
+
25
+ return unless attributes.values.compact.any?
26
+
27
+ new(**attributes)
28
+ end
29
+
30
+ def self.attribute_value(element, attribute_name)
31
+ value = element.attribute(attribute_name)&.value
32
+ return if value.nil? || value.strip.empty?
33
+
34
+ value
35
+ end
36
+ private_class_method :attribute_value
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,114 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing an EVENT element.
7
+ class Event
8
+ ATTRIBUTES = {
9
+ 'daytime' => { key: :daytime, required: false },
10
+ 'eventid' => { key: :event_id, required: true },
11
+ 'gender' => { key: :gender, required: false },
12
+ 'maxentries' => { key: :max_entries, required: false },
13
+ 'number' => { key: :number, required: true },
14
+ 'order' => { key: :order, required: false },
15
+ 'preveventid' => { key: :previous_event_id, required: false },
16
+ 'round' => { key: :round, required: false },
17
+ 'run' => { key: :run, required: false },
18
+ 'timing' => { key: :timing, required: false },
19
+ 'type' => { key: :type, required: false }
20
+ }.freeze
21
+
22
+ ATTRIBUTE_KEYS = ATTRIBUTES.values.map { |definition| definition[:key] }.freeze
23
+ private_constant :ATTRIBUTE_KEYS
24
+
25
+ ATTRIBUTE_KEYS.each { |attribute| attr_reader attribute }
26
+ attr_reader :fee, :swim_style, :age_groups, :heats, :time_standard_refs
27
+
28
+ def initialize(swim_style:, fee: nil, collections: {}, **attributes)
29
+ ATTRIBUTES.each_value do |definition|
30
+ key = definition[:key]
31
+ instance_variable_set(:"@#{key}", attributes[key])
32
+ end
33
+ @fee = fee
34
+ @swim_style = swim_style
35
+ @age_groups = Array(collections.fetch(:age_groups, []))
36
+ @heats = Array(collections.fetch(:heats, []))
37
+ @time_standard_refs = Array(collections.fetch(:time_standard_refs, []))
38
+ end
39
+
40
+ def self.from_xml(element)
41
+ raise ::Lenex::Parser::ParseError, 'EVENT element is required' unless element
42
+
43
+ attributes = extract_attributes(element)
44
+ fee = fee_from(element.at_xpath('FEE'))
45
+ swim_style = SwimStyle.from_xml(element.at_xpath('SWIMSTYLE'))
46
+ collections = build_collections(element)
47
+
48
+ new(**attributes, fee:, swim_style:, collections:)
49
+ end
50
+
51
+ def self.extract_attributes(element)
52
+ ATTRIBUTES.each_with_object({}) do |(attribute_name, definition), collected|
53
+ value = element.attribute(attribute_name)&.value
54
+ ensure_required_attribute!(attribute_name, definition, value)
55
+ collected[definition[:key]] = value if value
56
+ end
57
+ end
58
+ private_class_method :extract_attributes
59
+
60
+ def self.ensure_required_attribute!(attribute_name, definition, value)
61
+ return unless definition[:required]
62
+ return unless value.nil? || value.strip.empty?
63
+
64
+ message = "EVENT #{attribute_name} attribute is required"
65
+ raise ::Lenex::Parser::ParseError, message
66
+ end
67
+ private_class_method :ensure_required_attribute!
68
+
69
+ def self.fee_from(element)
70
+ return unless element
71
+
72
+ Fee.from_xml(element)
73
+ end
74
+ private_class_method :fee_from
75
+
76
+ def self.extract_age_groups(collection_element)
77
+ return [] unless collection_element
78
+
79
+ collection_element.xpath('AGEGROUP').map do |age_group_element|
80
+ AgeGroup.from_xml(age_group_element)
81
+ end
82
+ end
83
+ private_class_method :extract_age_groups
84
+
85
+ def self.extract_heats(collection_element)
86
+ return [] unless collection_element
87
+
88
+ collection_element.xpath('HEAT').map do |heat_element|
89
+ Heat.from_xml(heat_element)
90
+ end
91
+ end
92
+ private_class_method :extract_heats
93
+
94
+ def self.extract_time_standard_refs(collection_element)
95
+ return [] unless collection_element
96
+
97
+ collection_element.xpath('TIMESTANDARDREF').map do |time_standard_ref_element|
98
+ TimeStandardRef.from_xml(time_standard_ref_element)
99
+ end
100
+ end
101
+ private_class_method :extract_time_standard_refs
102
+
103
+ def self.build_collections(element)
104
+ {
105
+ age_groups: extract_age_groups(element.at_xpath('AGEGROUPS')),
106
+ heats: extract_heats(element.at_xpath('HEATS')),
107
+ time_standard_refs: extract_time_standard_refs(element.at_xpath('TIMESTANDARDREFS'))
108
+ }
109
+ end
110
+ private_class_method :build_collections
111
+ end
112
+ end
113
+ end
114
+ 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 FACILITY element.
7
+ class Facility
8
+ ATTRIBUTES = {
9
+ 'city' => { key: :city, required: true },
10
+ 'nation' => { key: :nation, required: true },
11
+ 'name' => { key: :name, required: false },
12
+ 'state' => { key: :state, required: false },
13
+ 'street' => { key: :street, required: false },
14
+ 'street2' => { key: :street2, required: false },
15
+ 'zip' => { key: :zip, 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, 'FACILITY 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 = "FACILITY #{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,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a FEE element.
7
+ class Fee
8
+ ATTRIBUTES = {
9
+ 'currency' => { key: :currency, required: false },
10
+ 'type' => { key: :type, required: false },
11
+ 'value' => { key: :value, 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, 'FEE 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 = "FEE #{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,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Lenex
4
+ module Parser
5
+ module Objects
6
+ # Value object representing a FEES collection.
7
+ class FeeSchedule
8
+ attr_reader :fees
9
+
10
+ def initialize(fees: [])
11
+ @fees = Array(fees)
12
+ end
13
+
14
+ def self.from_xml(element)
15
+ return unless element
16
+
17
+ fees = element.xpath('FEE').map do |fee_element|
18
+ Fee.from_xml(fee_element)
19
+ end
20
+
21
+ new(fees:)
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end