amsi 1.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.
- checksums.yaml +7 -0
- data/.ci +125 -0
- data/.gitignore +2 -0
- data/.rspec +3 -0
- data/CHANGELOG.txt +20 -0
- data/CODEOWNERS +1 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +21 -0
- data/README.md +319 -0
- data/Rakefile +6 -0
- data/amsi.gemspec +32 -0
- data/bin/console +15 -0
- data/bin/setup +8 -0
- data/config/multi_xml.rb +4 -0
- data/lib/amsi/attribute_parser/base.rb +23 -0
- data/lib/amsi/attribute_parser/boolean.rb +25 -0
- data/lib/amsi/attribute_parser/date.rb +28 -0
- data/lib/amsi/attribute_parser/date_time.rb +24 -0
- data/lib/amsi/attribute_parser/decimal.rb +15 -0
- data/lib/amsi/attribute_parser/integer.rb +15 -0
- data/lib/amsi/attribute_parser/string.rb +13 -0
- data/lib/amsi/attribute_parser.rb +45 -0
- data/lib/amsi/document_parser/base.rb +52 -0
- data/lib/amsi/document_parser/get_moveins.rb +66 -0
- data/lib/amsi/document_parser/guest_card_result.rb +31 -0
- data/lib/amsi/document_parser/leases.rb +46 -0
- data/lib/amsi/document_parser/properties.rb +103 -0
- data/lib/amsi/error/bad_request.rb +18 -0
- data/lib/amsi/error/base.rb +9 -0
- data/lib/amsi/error/invalid_response.rb +9 -0
- data/lib/amsi/error/request_fault.rb +18 -0
- data/lib/amsi/error/request_timeout.rb +9 -0
- data/lib/amsi/error/unparsable_response.rb +11 -0
- data/lib/amsi/model/address.rb +18 -0
- data/lib/amsi/model/base/attribute.rb +63 -0
- data/lib/amsi/model/base/attribute_store.rb +37 -0
- data/lib/amsi/model/base.rb +64 -0
- data/lib/amsi/model/guest_card.rb +18 -0
- data/lib/amsi/model/guest_card_result.rb +26 -0
- data/lib/amsi/model/lease.rb +63 -0
- data/lib/amsi/model/leasing_agent.rb +21 -0
- data/lib/amsi/model/manager.rb +16 -0
- data/lib/amsi/model/marketing_source.rb +21 -0
- data/lib/amsi/model/occupant.rb +32 -0
- data/lib/amsi/model/property.rb +35 -0
- data/lib/amsi/model/unit_type.rb +38 -0
- data/lib/amsi/parameter/contact_type.rb +10 -0
- data/lib/amsi/parameter/prospect.rb +19 -0
- data/lib/amsi/request/add_guest_card.rb +93 -0
- data/lib/amsi/request/base.rb +106 -0
- data/lib/amsi/request/get_moveins_by_first_marketing_source.rb +57 -0
- data/lib/amsi/request/get_property_list.rb +49 -0
- data/lib/amsi/request/get_property_residents.rb +45 -0
- data/lib/amsi/request_section/add_guest_card.rb +105 -0
- data/lib/amsi/request_section/auth.rb +22 -0
- data/lib/amsi/request_section/moveins_filter.rb +42 -0
- data/lib/amsi/request_section/property_list_filter.rb +45 -0
- data/lib/amsi/request_section/property_resident_filter.rb +32 -0
- data/lib/amsi/utils/request_fetcher.rb +37 -0
- data/lib/amsi/utils/request_generator.rb +54 -0
- data/lib/amsi/utils/snowflake_event_tracker.rb +113 -0
- data/lib/amsi/validator/base.rb +95 -0
- data/lib/amsi/validator/prospect_event_validator.rb +20 -0
- data/lib/amsi/validator/request_errors.rb +61 -0
- data/lib/amsi/validator/request_fault.rb +57 -0
- data/lib/amsi/validator/resident_event_validator.rb +20 -0
- data/lib/amsi/validator.rb +7 -0
- data/lib/amsi/version.rb +3 -0
- data/lib/amsi.rb +31 -0
- metadata +265 -0
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'tzinfo'
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Amsi
|
6
|
+
class AttributeParser
|
7
|
+
# Parse the response value of a date with time attribute.
|
8
|
+
class DateTime < Base
|
9
|
+
# AMSI time strings are assumed to be in Central time.
|
10
|
+
TIME_ZONE = TZInfo::Timezone.get('America/Chicago')
|
11
|
+
private_constant :TIME_ZONE
|
12
|
+
|
13
|
+
# @return [Date] the parsed attribute value
|
14
|
+
def parse
|
15
|
+
return if value == ''
|
16
|
+
date_time = ::DateTime.parse(value)
|
17
|
+
TIME_ZONE.local_to_utc(date_time) { |periods| periods.last }
|
18
|
+
rescue ArgumentError
|
19
|
+
raise Error::InvalidResponse,
|
20
|
+
"Invalid date/time response value: #{value}"
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Amsi
|
4
|
+
class AttributeParser
|
5
|
+
# Parse the response value of a decimal attribute
|
6
|
+
class Decimal < Base
|
7
|
+
# @return [Float] the parsed attribute value
|
8
|
+
def parse
|
9
|
+
Float(value)
|
10
|
+
rescue ArgumentError
|
11
|
+
raise Error::InvalidResponse, "Invalid decimal response value: #{value}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Amsi
|
4
|
+
class AttributeParser
|
5
|
+
# Parse the response value of an integer attribute
|
6
|
+
class Integer < Base
|
7
|
+
# @return [Integer] the parsed attribute value
|
8
|
+
def parse
|
9
|
+
Integer(value, 10)
|
10
|
+
rescue ArgumentError
|
11
|
+
raise Error::InvalidResponse, "Invalid integer response value: #{value}"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require 'amsi/attribute_parser/boolean'
|
2
|
+
require 'amsi/attribute_parser/date'
|
3
|
+
require 'amsi/attribute_parser/date_time'
|
4
|
+
require 'amsi/attribute_parser/decimal'
|
5
|
+
require 'amsi/attribute_parser/integer'
|
6
|
+
require 'amsi/attribute_parser/string'
|
7
|
+
|
8
|
+
module Amsi
|
9
|
+
# Parse the string value from the XML response into the configured data type
|
10
|
+
class AttributeParser
|
11
|
+
# @param value [String] the response value from AMSI
|
12
|
+
# @param type [Symbol] the attribute's configured data type
|
13
|
+
def initialize(value:, type:)
|
14
|
+
@value = value
|
15
|
+
@type = type
|
16
|
+
end
|
17
|
+
|
18
|
+
# @return [Object] the parsed attribute value
|
19
|
+
def parse
|
20
|
+
return unless value
|
21
|
+
case type
|
22
|
+
when :boolean
|
23
|
+
AttributeParser::Boolean.new(value).parse
|
24
|
+
when :date
|
25
|
+
AttributeParser::Date.new(value).parse
|
26
|
+
when :date_time
|
27
|
+
AttributeParser::DateTime.new(value).parse
|
28
|
+
when :decimal
|
29
|
+
AttributeParser::Decimal.new(value).parse
|
30
|
+
when :integer
|
31
|
+
AttributeParser::Integer.new(value).parse
|
32
|
+
when :string
|
33
|
+
AttributeParser::String.new(value).parse
|
34
|
+
else
|
35
|
+
raise ArgumentError, "Invalid attribute type: #{type}"
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
|
41
|
+
attr_reader :value, :type
|
42
|
+
end
|
43
|
+
|
44
|
+
private_constant :AttributeParser
|
45
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
require 'multi_xml'
|
2
|
+
require 'amsi/validator/prospect_event_validator'
|
3
|
+
require 'amsi/validator/resident_event_validator'
|
4
|
+
|
5
|
+
module Amsi
|
6
|
+
module DocumentParser
|
7
|
+
# Base class for parsing AMSI responses. Subclasses must implement
|
8
|
+
# #parse_body and can optionally override #validator_classes to add more
|
9
|
+
# validation than just the default
|
10
|
+
class Base
|
11
|
+
def initialize(params = {})
|
12
|
+
@params = params
|
13
|
+
end
|
14
|
+
|
15
|
+
# @param xml [String] the XML response from the request
|
16
|
+
# @return [Object] the parsed object(s) from the response
|
17
|
+
# @raise [Amsi::Error::Base] if the response is invalid
|
18
|
+
def parse(xml)
|
19
|
+
begin
|
20
|
+
parsed_response = MultiXml.parse(xml)
|
21
|
+
rescue MultiXml::ParseError => e
|
22
|
+
raise Amsi::Error::UnparsableResponse.new(xml)
|
23
|
+
end
|
24
|
+
|
25
|
+
validate_response!(parsed_response)
|
26
|
+
|
27
|
+
parse_body(parsed_response['soap:Envelope']['soap:Body'])
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def validate_response!(response_hash)
|
33
|
+
[*validator_classes].each do |klass|
|
34
|
+
klass.new(response_hash).validate!
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
39
|
+
# into a Hash
|
40
|
+
# @return [Object] the parsed object(s) from the response
|
41
|
+
# @raise [Amsi::Error::Base] if the response is invalid
|
42
|
+
def parse_body(body)
|
43
|
+
raise NotImplementedError,
|
44
|
+
"#{self.class.name} must implement #{__method__}"
|
45
|
+
end
|
46
|
+
|
47
|
+
def validator_classes
|
48
|
+
[Validator::RequestErrors, Validator::RequestFault, Validator::ResidentEventValidator, Validator::ProspectEventValidator]
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
require 'amsi/validator/request_errors'
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Amsi
|
6
|
+
module DocumentParser
|
7
|
+
# Parse the GetMoveinsByFirstMarketingSource response
|
8
|
+
class GetMoveins < Base
|
9
|
+
private
|
10
|
+
|
11
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
12
|
+
# into a Hash
|
13
|
+
# @return [Array<Amsi::Model::Lease>] the leases contained in the
|
14
|
+
# response
|
15
|
+
# @raise [Amsi::Error::Base] if the response is invalid
|
16
|
+
def parse_body(body)
|
17
|
+
lease_hashes(body).map do |lease_hash|
|
18
|
+
lease = Model::Lease.new(lease_hash)
|
19
|
+
lease.occupants = occupants(lease_hash)
|
20
|
+
lease.guest_card = guest_card(lease_hash)
|
21
|
+
lease.matched_guest_cards = matched_guest_cards(lease_hash)
|
22
|
+
|
23
|
+
lease
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
27
|
+
def lease_hashes(body)
|
28
|
+
response = body['GetMoveinsByFirstMarketingSourceResponse']
|
29
|
+
escaped_result = response['GetMoveinsByFirstMarketingSourceResult']
|
30
|
+
parsed_result = MultiXml.parse(escaped_result)
|
31
|
+
leases = parsed_result['Leases']['Lease']
|
32
|
+
|
33
|
+
return [] if leases.nil?
|
34
|
+
leases.is_a?(Array) ? leases : [leases]
|
35
|
+
end
|
36
|
+
|
37
|
+
def occupants(lease_hash)
|
38
|
+
occupant_hashes(lease_hash).map do |occupant_hash|
|
39
|
+
Model::Occupant.new(occupant_hash)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def occupant_hashes(lease_hash)
|
44
|
+
occupants = lease_hash['Occupant']
|
45
|
+
|
46
|
+
return [] if occupants.nil?
|
47
|
+
occupants.is_a?(Array) ? occupants : [occupants]
|
48
|
+
end
|
49
|
+
|
50
|
+
def guest_card(lease_hash)
|
51
|
+
guest_card_hash = lease_hash['GuestCard']
|
52
|
+
Model::GuestCard.new(guest_card_hash) unless guest_card_hash.nil?
|
53
|
+
end
|
54
|
+
|
55
|
+
def matched_guest_cards(lease_hash)
|
56
|
+
matched_guest_card_hashes = lease_hash['MatchedGuestCard']
|
57
|
+
|
58
|
+
if matched_guest_card_hashes.is_a?(Hash)
|
59
|
+
[matched_guest_card_hashes]
|
60
|
+
else
|
61
|
+
[*matched_guest_card_hashes]
|
62
|
+
end.map { |h| Model::GuestCard.new(h) }
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'amsi/validator/request_errors'
|
2
|
+
require 'amsi/validator/request_fault'
|
3
|
+
require 'amsi/error/bad_request'
|
4
|
+
|
5
|
+
require_relative 'base'
|
6
|
+
|
7
|
+
module Amsi
|
8
|
+
module DocumentParser
|
9
|
+
# Parse the AddGuestCard response
|
10
|
+
class GuestCardResult < Base
|
11
|
+
private
|
12
|
+
|
13
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
14
|
+
# into a Hash
|
15
|
+
# @return [Array<Amsi::Model::Lease>] the leases contained in the
|
16
|
+
# response
|
17
|
+
# @raise [Amsi::Error::Base] if the response is invalid
|
18
|
+
def parse_body(body)
|
19
|
+
response = body['AddGuestCardResponse']
|
20
|
+
escaped_result = response['AddGuestCardResult']
|
21
|
+
parsed_result = MultiXml.parse(escaped_result)['EDEX']
|
22
|
+
Model::GuestCardResult.new(
|
23
|
+
contact_seq_no: parsed_result['contactseqno'],
|
24
|
+
guest_card_id: parsed_result['guestcardid'],
|
25
|
+
property_id: parsed_result['propertyid'],
|
26
|
+
status: parsed_result['status']
|
27
|
+
)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'amsi/validator/request_errors'
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Amsi
|
6
|
+
module DocumentParser
|
7
|
+
# Parse the GetPropertyResidents response
|
8
|
+
class Leases < Base
|
9
|
+
private
|
10
|
+
|
11
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
12
|
+
# into a Hash
|
13
|
+
# @return [Array<Amsi::Model::Lease>] the leases contained in the
|
14
|
+
# response
|
15
|
+
# @raise [Amsi::Error::Base] if the response is invalid
|
16
|
+
def parse_body(body)
|
17
|
+
lease_hashes(body).map do |lease_hash|
|
18
|
+
lease = Model::Lease.new(lease_hash)
|
19
|
+
lease.occupants = occupant_hashes(lease_hash).map do |occupant_hash|
|
20
|
+
Model::Occupant.new(occupant_hash)
|
21
|
+
end
|
22
|
+
Validator::ResidentEventValidator.new.send_event(lease, @params)
|
23
|
+
Validator::ProspectEventValidator.new.send_event(lease, @params)
|
24
|
+
lease
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def lease_hashes(body)
|
29
|
+
response = body['GetPropertyResidentsResponse']
|
30
|
+
escaped_result = response['GetPropertyResidentsResult']
|
31
|
+
parsed_result = MultiXml.parse(escaped_result)
|
32
|
+
leases = parsed_result['PropertyResidents']['Lease']
|
33
|
+
|
34
|
+
return [] if leases.nil?
|
35
|
+
leases.is_a?(Array) ? leases : [leases]
|
36
|
+
end
|
37
|
+
|
38
|
+
def occupant_hashes(lease_hash)
|
39
|
+
occupants = lease_hash['Occupant']
|
40
|
+
|
41
|
+
return [] if occupants.nil?
|
42
|
+
occupants.is_a?(Array) ? occupants : [occupants]
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
require 'amsi/validator/request_errors'
|
2
|
+
|
3
|
+
require_relative 'base'
|
4
|
+
|
5
|
+
module Amsi
|
6
|
+
module DocumentParser
|
7
|
+
# Parse the GetPropertyList response
|
8
|
+
class Properties < Base
|
9
|
+
ADDRESS_ATTRS = %i[
|
10
|
+
Line1
|
11
|
+
Line2
|
12
|
+
Line3
|
13
|
+
Line4
|
14
|
+
City
|
15
|
+
State
|
16
|
+
ZipCode
|
17
|
+
Country
|
18
|
+
].freeze
|
19
|
+
private_constant :ADDRESS_ATTRS
|
20
|
+
|
21
|
+
MANAGER_ATTR_MAP = {
|
22
|
+
'MgrOffPhoneNo' => 'phone',
|
23
|
+
'MgrFaxNo' => 'fax',
|
24
|
+
'MgrSalutation' => 'salutation',
|
25
|
+
'MgrFirstName' => 'first_name',
|
26
|
+
'MgrMiName' => 'middle_name',
|
27
|
+
'MgrLastName' => 'last_name'
|
28
|
+
}.freeze
|
29
|
+
private_constant :MANAGER_ATTR_MAP
|
30
|
+
|
31
|
+
private
|
32
|
+
|
33
|
+
# @param body [Hash<String, Object>] the body of the XML response parsed
|
34
|
+
# into a Hash
|
35
|
+
# @return [Array<Amsi::Model::Property>] the properties contained in
|
36
|
+
# the response
|
37
|
+
# @raise [Amsi::Error::Base] if the response is invalid
|
38
|
+
def parse_body(body)
|
39
|
+
property_hashes(body).map do |property_hash|
|
40
|
+
property = Model::Property.new(property_hash)
|
41
|
+
property.address = address(property_hash, 'Property')
|
42
|
+
property.remit_to_address = address(property_hash, 'RemitTo')
|
43
|
+
property.manager = manager(property_hash)
|
44
|
+
property.leasing_agents = leasing_agents(property_hash)
|
45
|
+
property.marketing_sources = marketing_sources(property_hash)
|
46
|
+
property.unit_types = unit_types(property_hash)
|
47
|
+
property
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def address(property_hash, prefix)
|
52
|
+
address_attrs = ADDRESS_ATTRS.map.with_object({}) do |field, hash|
|
53
|
+
hash[field] = property_hash["#{prefix}Addr#{field}"]
|
54
|
+
end
|
55
|
+
Model::Address.new(address_attrs)
|
56
|
+
end
|
57
|
+
|
58
|
+
def manager(property_hash)
|
59
|
+
manager_attrs = MANAGER_ATTR_MAP.map.with_object({}) do |(node, attr), hash|
|
60
|
+
hash[attr] = property_hash[node]
|
61
|
+
end
|
62
|
+
Model::Manager.new(manager_attrs)
|
63
|
+
end
|
64
|
+
|
65
|
+
def leasing_agents(property_hash)
|
66
|
+
agent_hashes = property_hash['LeasingAgent']
|
67
|
+
return [] if agent_hashes.nil?
|
68
|
+
agent_hashes = [agent_hashes] unless agent_hashes.is_a?(Array)
|
69
|
+
agent_hashes.map do |agent_hash|
|
70
|
+
Model::LeasingAgent.new(agent_hash)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def marketing_sources(property_hash)
|
75
|
+
source_hashes = property_hash['MarketingSource']
|
76
|
+
return [] if source_hashes.nil?
|
77
|
+
source_hashes = [source_hashes] unless source_hashes.is_a?(Array)
|
78
|
+
source_hashes.map do |source_hash|
|
79
|
+
Model::MarketingSource.new(source_hash)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
def unit_types(property_hash)
|
84
|
+
type_hashes = property_hash['UnitType']
|
85
|
+
return [] if type_hashes.nil?
|
86
|
+
type_hashes = [type_hashes] unless type_hashes.is_a?(Array)
|
87
|
+
type_hashes.map do |type_hash|
|
88
|
+
Model::UnitType.new(type_hash)
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def property_hashes(body)
|
93
|
+
response = body['GetPropertyListResponse']
|
94
|
+
escaped_result = response['GetPropertyListResult']
|
95
|
+
parsed_result = MultiXml.parse(escaped_result)
|
96
|
+
properties = parsed_result['Properties']['Property']
|
97
|
+
|
98
|
+
return [] if properties.nil?
|
99
|
+
properties.is_a?(Array) ? properties : [properties]
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Amsi
|
4
|
+
module Error
|
5
|
+
# Raised when an AMSI request has an error node in the contents. This
|
6
|
+
# generally happens when one of the parameters in a section other than the
|
7
|
+
# auth section is invalid.
|
8
|
+
class BadRequest < Base
|
9
|
+
attr_reader :errors
|
10
|
+
|
11
|
+
# @param errors [Object] object must respond to message
|
12
|
+
def initialize(errors)
|
13
|
+
super(errors.map(&:message).join('; '))
|
14
|
+
@errors = errors
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
require_relative 'base'
|
2
|
+
|
3
|
+
module Amsi
|
4
|
+
module Error
|
5
|
+
# Raised when a AMSI request has a fault node, which general appears
|
6
|
+
# when there is an issue with the auth section in the request (e.g.
|
7
|
+
# invalid password)
|
8
|
+
class RequestFault < Base
|
9
|
+
attr_reader :fault_code, :details
|
10
|
+
|
11
|
+
def initialize(message, fault_code, details = nil)
|
12
|
+
super(message)
|
13
|
+
@fault_code = fault_code
|
14
|
+
@details = details
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
module Amsi
|
2
|
+
module Model
|
3
|
+
class Base
|
4
|
+
# Encapsulate a model attribute
|
5
|
+
class Attribute
|
6
|
+
attr_reader :prefix, :type, :name
|
7
|
+
|
8
|
+
def initialize(prefix:, type:, name:)
|
9
|
+
@prefix = prefix
|
10
|
+
@type = type
|
11
|
+
@name = name.to_s
|
12
|
+
end
|
13
|
+
|
14
|
+
# @return [String] the name of the method on the model used to access
|
15
|
+
# this attribute
|
16
|
+
def accessor_name
|
17
|
+
type == :boolean ? "#{name}?" : name
|
18
|
+
end
|
19
|
+
|
20
|
+
# There is some magic happening here to support more convenient
|
21
|
+
# attribute names. Since Model::Base.new can take parameters from a
|
22
|
+
# parsed XML document or from a human, we need to be able to look up
|
23
|
+
# the attribute based on either. The general mapping looks like:
|
24
|
+
#
|
25
|
+
# XML node | attribute name
|
26
|
+
# ---------------+--------------------
|
27
|
+
# SomeThing | some_thing
|
28
|
+
# something | some_thing
|
29
|
+
# SomeThingID | some_thing_id
|
30
|
+
# somethingID | some_thing_id
|
31
|
+
# foobarid | bar_id (if in Model::Foo)
|
32
|
+
# somethingbit | some_thing (if defined as a :boolean)
|
33
|
+
# somethingflag | some_thing (if defined as a :boolean)
|
34
|
+
#
|
35
|
+
# @param attr [String|Symbol] the name of the attribute or the XML node
|
36
|
+
# name from the AMSI response, e.g. :made_ready_date or
|
37
|
+
# "MadeReadyDate"
|
38
|
+
# @return [true|false] true iff the value passed in is for this
|
39
|
+
# Attribute
|
40
|
+
def matches?(attr)
|
41
|
+
possible_names.include?(attr.to_s.downcase)
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def possible_names
|
47
|
+
squished_name = name.gsub(/_/, '')
|
48
|
+
prefixed_name = "#{prefix}#{squished_name}"
|
49
|
+
names = [name, squished_name, prefixed_name]
|
50
|
+
if name.end_with?('id')
|
51
|
+
big_id_names = [squished_name.gsub(/id$/, 'ID'),
|
52
|
+
prefixed_name.gsub(/id$/, 'ID')]
|
53
|
+
names.concat(big_id_names)
|
54
|
+
end
|
55
|
+
if type == :boolean
|
56
|
+
names.concat(["#{squished_name}flag", "#{squished_name}bit"])
|
57
|
+
end
|
58
|
+
names
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require_relative 'attribute'
|
2
|
+
|
3
|
+
module Amsi
|
4
|
+
module Model
|
5
|
+
class Base
|
6
|
+
# Hold attributes defined with the DSL in Models
|
7
|
+
class AttributeStore
|
8
|
+
def initialize(model_class)
|
9
|
+
@model_class = model_class
|
10
|
+
@attributes = []
|
11
|
+
end
|
12
|
+
|
13
|
+
# Add all attributes of the same type to the AttributeStore
|
14
|
+
def add(type, attrs)
|
15
|
+
attrs.map do |attr|
|
16
|
+
attribute = Attribute.new(prefix: prefix, type: type, name: attr)
|
17
|
+
@attributes << attribute
|
18
|
+
attribute
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
# Find a specific attribute with the given name
|
23
|
+
def find(attr)
|
24
|
+
attributes.detect { |attribute| attribute.matches?(attr) }
|
25
|
+
end
|
26
|
+
|
27
|
+
private
|
28
|
+
|
29
|
+
attr_reader :attributes, :model_class
|
30
|
+
|
31
|
+
def prefix
|
32
|
+
''
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
require 'amsi/attribute_parser'
|
2
|
+
require_relative 'base/attribute_store'
|
3
|
+
|
4
|
+
module Amsi
|
5
|
+
module Model
|
6
|
+
# Base class for models, which are the return values from Requests
|
7
|
+
class Base
|
8
|
+
class << self
|
9
|
+
attr_accessor :attribute_store
|
10
|
+
end
|
11
|
+
|
12
|
+
# Create a DSL to define the models attributes and their data types. This
|
13
|
+
# bit of meta-programming will create a class method for each data type
|
14
|
+
# to define the model's attributes of the type, e.g.
|
15
|
+
#
|
16
|
+
# class LostAndFound < Base
|
17
|
+
# date_attr :found_date, :lost_date
|
18
|
+
# end
|
19
|
+
#
|
20
|
+
# When a new LostAndFound is initialized (from an AMSI response hash,
|
21
|
+
# that may have the keys 'founddate' and 'lostdate'), it will create
|
22
|
+
# attribute readers for #found_date and #lost_date that return Date
|
23
|
+
# instances.
|
24
|
+
#
|
25
|
+
# Boolean attributes will have the existential '?', e.g.
|
26
|
+
#
|
27
|
+
# class LostAndFound < Base
|
28
|
+
# boolean_attr :found
|
29
|
+
# end
|
30
|
+
#
|
31
|
+
# lost_and_found = LostAndFound.new('foundbit' => '1')
|
32
|
+
# lost_and_found.found?
|
33
|
+
# # => true
|
34
|
+
%i[
|
35
|
+
boolean date date_time decimal integer object string phone
|
36
|
+
].each do |data_type|
|
37
|
+
define_singleton_method "#{data_type}_attrs" do |*attrs|
|
38
|
+
self.attribute_store ||= AttributeStore.new(self)
|
39
|
+
attribute_store.add(data_type, attrs).each do |attribute|
|
40
|
+
define_method attribute.accessor_name do
|
41
|
+
instance_variable_get("@#{attribute.name}")
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
# Initialize a new model with the response hash from AMSI. Attribute
|
48
|
+
# values are parsed into their configured data types.
|
49
|
+
#
|
50
|
+
# @param attrs [Hash<String, Object>] the response hash from AMSI.
|
51
|
+
# Attribute keys are case insensitive.
|
52
|
+
def initialize(attrs = {})
|
53
|
+
attrs.each do |attr, value|
|
54
|
+
attribute = self.class.attribute_store.find(attr)
|
55
|
+
next unless attribute
|
56
|
+
parser = AttributeParser.new(value: value, type: attribute.type)
|
57
|
+
instance_variable_set("@#{attribute.name}", parser.parse)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
private_constant :Base
|
63
|
+
end
|
64
|
+
end
|