amsi 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. checksums.yaml +7 -0
  2. data/.ci +125 -0
  3. data/.gitignore +2 -0
  4. data/.rspec +3 -0
  5. data/CHANGELOG.txt +20 -0
  6. data/CODEOWNERS +1 -0
  7. data/Gemfile +6 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +319 -0
  10. data/Rakefile +6 -0
  11. data/amsi.gemspec +32 -0
  12. data/bin/console +15 -0
  13. data/bin/setup +8 -0
  14. data/config/multi_xml.rb +4 -0
  15. data/lib/amsi/attribute_parser/base.rb +23 -0
  16. data/lib/amsi/attribute_parser/boolean.rb +25 -0
  17. data/lib/amsi/attribute_parser/date.rb +28 -0
  18. data/lib/amsi/attribute_parser/date_time.rb +24 -0
  19. data/lib/amsi/attribute_parser/decimal.rb +15 -0
  20. data/lib/amsi/attribute_parser/integer.rb +15 -0
  21. data/lib/amsi/attribute_parser/string.rb +13 -0
  22. data/lib/amsi/attribute_parser.rb +45 -0
  23. data/lib/amsi/document_parser/base.rb +52 -0
  24. data/lib/amsi/document_parser/get_moveins.rb +66 -0
  25. data/lib/amsi/document_parser/guest_card_result.rb +31 -0
  26. data/lib/amsi/document_parser/leases.rb +46 -0
  27. data/lib/amsi/document_parser/properties.rb +103 -0
  28. data/lib/amsi/error/bad_request.rb +18 -0
  29. data/lib/amsi/error/base.rb +9 -0
  30. data/lib/amsi/error/invalid_response.rb +9 -0
  31. data/lib/amsi/error/request_fault.rb +18 -0
  32. data/lib/amsi/error/request_timeout.rb +9 -0
  33. data/lib/amsi/error/unparsable_response.rb +11 -0
  34. data/lib/amsi/model/address.rb +18 -0
  35. data/lib/amsi/model/base/attribute.rb +63 -0
  36. data/lib/amsi/model/base/attribute_store.rb +37 -0
  37. data/lib/amsi/model/base.rb +64 -0
  38. data/lib/amsi/model/guest_card.rb +18 -0
  39. data/lib/amsi/model/guest_card_result.rb +26 -0
  40. data/lib/amsi/model/lease.rb +63 -0
  41. data/lib/amsi/model/leasing_agent.rb +21 -0
  42. data/lib/amsi/model/manager.rb +16 -0
  43. data/lib/amsi/model/marketing_source.rb +21 -0
  44. data/lib/amsi/model/occupant.rb +32 -0
  45. data/lib/amsi/model/property.rb +35 -0
  46. data/lib/amsi/model/unit_type.rb +38 -0
  47. data/lib/amsi/parameter/contact_type.rb +10 -0
  48. data/lib/amsi/parameter/prospect.rb +19 -0
  49. data/lib/amsi/request/add_guest_card.rb +93 -0
  50. data/lib/amsi/request/base.rb +106 -0
  51. data/lib/amsi/request/get_moveins_by_first_marketing_source.rb +57 -0
  52. data/lib/amsi/request/get_property_list.rb +49 -0
  53. data/lib/amsi/request/get_property_residents.rb +45 -0
  54. data/lib/amsi/request_section/add_guest_card.rb +105 -0
  55. data/lib/amsi/request_section/auth.rb +22 -0
  56. data/lib/amsi/request_section/moveins_filter.rb +42 -0
  57. data/lib/amsi/request_section/property_list_filter.rb +45 -0
  58. data/lib/amsi/request_section/property_resident_filter.rb +32 -0
  59. data/lib/amsi/utils/request_fetcher.rb +37 -0
  60. data/lib/amsi/utils/request_generator.rb +54 -0
  61. data/lib/amsi/utils/snowflake_event_tracker.rb +113 -0
  62. data/lib/amsi/validator/base.rb +95 -0
  63. data/lib/amsi/validator/prospect_event_validator.rb +20 -0
  64. data/lib/amsi/validator/request_errors.rb +61 -0
  65. data/lib/amsi/validator/request_fault.rb +57 -0
  66. data/lib/amsi/validator/resident_event_validator.rb +20 -0
  67. data/lib/amsi/validator.rb +7 -0
  68. data/lib/amsi/version.rb +3 -0
  69. data/lib/amsi.rb +31 -0
  70. 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,13 @@
1
+ require_relative 'base'
2
+
3
+ module Amsi
4
+ class AttributeParser
5
+ # Parse the response value of a string attribute
6
+ class String < Base
7
+ # @return [String] the parsed attribute value
8
+ def parse
9
+ value
10
+ end
11
+ end
12
+ end
13
+ 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,9 @@
1
+ module Amsi
2
+ module Error
3
+ # A base class for all errors that originate from this gem
4
+ class Base < StandardError
5
+ end
6
+
7
+ private_constant :Base
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ require_relative 'base'
2
+
3
+ module Amsi
4
+ module Error
5
+ # Raised when we cannot parse the response
6
+ class InvalidResponse < Base
7
+ end
8
+ end
9
+ 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,9 @@
1
+ require_relative 'base'
2
+
3
+ module Amsi
4
+ module Error
5
+ # Raised when a AMSI request times out
6
+ class RequestTimeout < Base
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ require_relative 'base'
2
+
3
+ module Amsi
4
+ module Error
5
+ ##
6
+ # An Error that is raised when Amsi response cannot be parsed.
7
+ #
8
+ class UnparsableResponse < Base
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,18 @@
1
+ require_relative 'base'
2
+
3
+ module Amsi
4
+ module Model
5
+ class Address < Base
6
+ string_attrs *%i[
7
+ line_1
8
+ line_2
9
+ line_3
10
+ line_4
11
+ city
12
+ state
13
+ zip_code
14
+ country
15
+ ]
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