amsi 1.0.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|