diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index a174d05eda8..6f3b40f4d52 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -745,6 +745,7 @@ config/initializers/clamav.rb @department-of-veterans-affairs/va-api-engineers @ config/initializers/combine_pdf_log_patch.rb @department-of-veterans-affairs/backend-review-group config/initializers/config.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/initializers/cookie_rotation.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group +config/initializers/core_extensions.rb @department-of-veterans-affairs/backend-review-group config/initializers/covid_vaccine_facilities.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group @department-of-veterans-affairs/long-covid config/initializers/datadog.rb @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group config/initializers/date_formats.rb @department-of-veterans-affairs/octo-identity @@ -864,6 +865,7 @@ lib/common/pdf_helpers.rb @department-of-veterans-affairs/benefits-decision-revi lib/common/models @department-of-veterans-affairs/backend-review-group lib/common/virus_scan.rb @department-of-veterans-affairs/backend-review-group lib/common @department-of-veterans-affairs/backend-review-group +lib/core_extensions @department-of-veterans-affairs/backend-review-group lib/debt_management_center @department-of-veterans-affairs/vsa-debt-resolution @department-of-veterans-affairs/backend-review-group lib/decision_review @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group lib/decision_review_v1 @department-of-veterans-affairs/benefits-decision-reviews-be @department-of-veterans-affairs/va-api-engineers @department-of-veterans-affairs/backend-review-group diff --git a/app/models/preneeds/address.rb b/app/models/preneeds/address.rb index 9f5e6d46c30..f1c94f9dc9b 100644 --- a/app/models/preneeds/address.rb +++ b/app/models/preneeds/address.rb @@ -16,7 +16,7 @@ module Preneeds # @!attribute postal_code # @return [String] address postal code # - class Address < Preneeds::VirtusBase + class Address < Preneeds::Base attribute :street, String attribute :street2, String attribute :city, String diff --git a/app/models/preneeds/applicant.rb b/app/models/preneeds/applicant.rb index f75bb1647f4..fca86d04b08 100644 --- a/app/models/preneeds/applicant.rb +++ b/app/models/preneeds/applicant.rb @@ -18,7 +18,7 @@ module Preneeds # @!attribute name # @return [Preneeds::FullName] applicant's name # - class Applicant < Preneeds::VirtusBase + class Applicant < Preneeds::Base attribute :applicant_email, String attribute :applicant_phone_number, String attribute :applicant_relationship_to_claimant, String diff --git a/app/models/preneeds/base.rb b/app/models/preneeds/base.rb new file mode 100644 index 00000000000..4c426c03632 --- /dev/null +++ b/app/models/preneeds/base.rb @@ -0,0 +1,136 @@ +# frozen_string_literal: true + +# This will be moved after virtus is removed +module Boolean; end +class TrueClass; include Boolean; end +class FalseClass; include Boolean; end + +# Parent class for other Preneeds Burial form related models +# Should not be initialized directly +# +module Preneeds + class Base + extend ActiveModel::Naming + include ActiveModel::Model + include ActiveModel::Serializers::JSON + + @attributes = {}.freeze + + class << self + # class variable attributes won't work so this is + # the only way for it to work. Thread safety shouldn't + # matter because @attributes is the same across all thread + # they are set by the class. + # rubocop:disable ThreadSafety/InstanceVariableInClassMethod + def attributes + @attributes ||= {} + end + # rubocop:enable ThreadSafety/InstanceVariableInClassMethod + + # Class method to define a setter & getter for attribute + # this will also coerce a hash to the require class + # doesn't currently coerce scalar classes such as string to int + # In the future this could become it's own class e.g., Vets::Model::Attribute + # + # @param name [Symbol] the name of the attribute + # @param klass [Class] the class of the attribute + # @param default [String|Integer] the default value of the attribute + # + def attribute(name, klass, **options) + default = options[:default] + array = options[:array] || false + + attributes[name] = { type: klass, default:, array: } + + define_getter(name, default) + define_setter(name, klass, array) + end + + def attribute_set + attributes.keys + end + + private + + def define_getter(name, default) + define_method(name) do + instance_variable_get("@#{name}") || begin + return nil unless defined?(default) + + if default.is_a?(Symbol) && respond_to?(default) + send(default) + else + default + end + end + end + end + + def define_setter(name, klass, array) + define_method("#{name}=") do |value| + if array + raise TypeError, "#{name} must be an Array" unless value.is_a?(Array) + + value = value.map do |item| + item.is_a?(Hash) ? klass.new(item) : item + end + + unless value.all? { |item| item.is_a?(klass) } + raise TypeError, "All elements of #{name} must be of type #{klass}" + end + end + + value = ActiveModel::Type::Boolean.new.cast(value) if klass == Boolean + + value = klass.new(value) if value.is_a?(Hash) + + if (array && value.is_a?(Array)) || value.is_a?(klass) || value.nil? + instance_variable_set("@#{name}", value) + else + raise TypeError, "#{name} must be a #{klass}" + end + end + end + end + + def initialize(params = {}) + super + # Ensure all attributes have a defined value (default to nil) + self.class.attribute_set.each do |attr_name| + instance_variable_set("@#{attr_name}", nil) unless instance_variable_defined?("@#{attr_name}") + end + end + + # Acts as ActiveRecord::Base#attributes which is needed for serialization + def attributes + nested_attributes(instance_values) + end + + # Override `as_json` + # + # @param options [Hash] + # + # @see ActiveModel::Serializers::JSON + # + def as_json(options = {}) + super(options).deep_transform_keys { |key| key.camelize(:lower) } + end + + private + + # Collect values from attribute and nested objects + # + # @param values [Hash] + # + # @return [Hash] nested attributes + def nested_attributes(values) + values.transform_values do |value| + if value.respond_to?(:instance_values) + nested_attributes(value.instance_values) + else + value + end + end + end + end +end diff --git a/app/models/preneeds/burial_form.rb b/app/models/preneeds/burial_form.rb index 0713a1cfc00..122d87ad472 100644 --- a/app/models/preneeds/burial_form.rb +++ b/app/models/preneeds/burial_form.rb @@ -33,13 +33,13 @@ module Preneeds # @!attribute veteran # @return [Preneeds::Veteran] Veteran object. Veteran is the person who is the owner of the benefit. # - class BurialForm < Preneeds::VirtusBase + class BurialForm < Preneeds::Base # Preneeds Burial Form official form id # FORM = '40-10007' attribute :application_status, String, default: '' - attribute :preneed_attachments, Array[PreneedAttachmentHash] + attribute :preneed_attachments, PreneedAttachmentHash, array: true attribute :has_currently_buried, String attribute :sending_application, String, default: 'vets.gov' attribute :sending_code, String, default: '' @@ -47,7 +47,7 @@ class BurialForm < Preneeds::VirtusBase attribute :tracking_number, String, default: :generate_tracking_number attribute :applicant, Preneeds::Applicant attribute :claimant, Preneeds::Claimant - attribute :currently_buried_persons, Array[Preneeds::CurrentlyBuriedPerson] + attribute :currently_buried_persons, Preneeds::CurrentlyBuriedPerson, array: true attribute :veteran, Preneeds::Veteran # keeping this name because it matches the previous attribute diff --git a/app/models/preneeds/claimant.rb b/app/models/preneeds/claimant.rb index fcbaa74aef9..d198874e46b 100644 --- a/app/models/preneeds/claimant.rb +++ b/app/models/preneeds/claimant.rb @@ -22,7 +22,7 @@ module Preneeds # @!attribute address # @return [Preneeds::Address] claimant's address # - class Claimant < Preneeds::VirtusBase + class Claimant < Preneeds::Base attribute :date_of_birth, String attribute :desired_cemetery, String attribute :email, String diff --git a/app/models/preneeds/currently_buried_person.rb b/app/models/preneeds/currently_buried_person.rb index b4fe917c83a..b5e8023ce26 100644 --- a/app/models/preneeds/currently_buried_person.rb +++ b/app/models/preneeds/currently_buried_person.rb @@ -10,7 +10,7 @@ module Preneeds # @!attribute name # @return [Preneeds::FullName] currently buried person's full name # - class CurrentlyBuriedPerson < Preneeds::VirtusBase + class CurrentlyBuriedPerson < Preneeds::Base attribute :cemetery_number, String attribute :name, Preneeds::FullName diff --git a/app/models/preneeds/date_range.rb b/app/models/preneeds/date_range.rb index 104645dd7c6..f1bcbe8790a 100644 --- a/app/models/preneeds/date_range.rb +++ b/app/models/preneeds/date_range.rb @@ -8,7 +8,7 @@ module Preneeds # @!attribute to # @return [String] 'to' date # - class DateRange < Preneeds::VirtusBase + class DateRange < Preneeds::Base attribute :from, String attribute :to, String diff --git a/app/models/preneeds/full_name.rb b/app/models/preneeds/full_name.rb index aa3b87157bf..feb1f1c8844 100644 --- a/app/models/preneeds/full_name.rb +++ b/app/models/preneeds/full_name.rb @@ -16,7 +16,7 @@ module Preneeds # @!attribute suffix # @return [String] name suffix # - class FullName < Preneeds::VirtusBase + class FullName < Preneeds::Base attribute :first, String attribute :last, String attribute :maiden, String diff --git a/app/models/preneeds/preneed_attachment_hash.rb b/app/models/preneeds/preneed_attachment_hash.rb index 1ddd297a07e..7cac5fe5d6c 100644 --- a/app/models/preneeds/preneed_attachment_hash.rb +++ b/app/models/preneeds/preneed_attachment_hash.rb @@ -11,7 +11,7 @@ module Preneeds # @!attribute name # @return [String] attachment file name # - class PreneedAttachmentHash < Preneeds::VirtusBase + class PreneedAttachmentHash < Preneeds::Base attribute :confirmation_code, String attribute :attachment_id, String attribute :name, String diff --git a/app/models/preneeds/race.rb b/app/models/preneeds/race.rb index 1388d2209b4..0c9d82720ce 100644 --- a/app/models/preneeds/race.rb +++ b/app/models/preneeds/race.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true module Preneeds - class Race < Preneeds::VirtusBase + class Race < Preneeds::Base ATTRIBUTE_MAPPING = { 'I' => :is_american_indian_or_alaskan_native, 'A' => :is_asian, @@ -13,7 +13,7 @@ class Race < Preneeds::VirtusBase }.freeze ATTRIBUTE_MAPPING.each_value do |attr| - attribute(attr, Boolean) + attribute(attr, Boolean, default: false) end def as_eoas diff --git a/app/models/preneeds/service_record.rb b/app/models/preneeds/service_record.rb index 89f4f85a39b..04aa252c762 100644 --- a/app/models/preneeds/service_record.rb +++ b/app/models/preneeds/service_record.rb @@ -16,7 +16,7 @@ module Preneeds # @!attribute date_range # @return [Preneeds::DateRange] service date range # - class ServiceRecord < Preneeds::VirtusBase + class ServiceRecord < Preneeds::Base attribute :service_branch, String attribute :discharge_type, String attribute :highest_rank, String @@ -29,8 +29,8 @@ class ServiceRecord < Preneeds::VirtusBase def as_eoas hash = { branchOfService: service_branch, dischargeType: discharge_type, - enteredOnDutyDate: date_range.try(:[], :from), highestRank: highest_rank, - nationalGuardState: national_guard_state, releaseFromDutyDate: date_range.try(:[], :to) + enteredOnDutyDate: date_range.try(:from), highestRank: highest_rank, + nationalGuardState: national_guard_state, releaseFromDutyDate: date_range.try(:to) } %i[ diff --git a/app/models/preneeds/veteran.rb b/app/models/preneeds/veteran.rb index 8fb99066903..5ae0d46c073 100644 --- a/app/models/preneeds/veteran.rb +++ b/app/models/preneeds/veteran.rb @@ -34,7 +34,7 @@ module Preneeds # @!attribute service_records # @return [Array] veteran's service records # - class Veteran < Preneeds::VirtusBase + class Veteran < Preneeds::Base attribute :date_of_birth, String attribute :date_of_death, String attribute :gender, String @@ -50,7 +50,7 @@ class Veteran < Preneeds::VirtusBase attribute :address, Preneeds::Address attribute :current_name, Preneeds::FullName attribute :service_name, Preneeds::FullName - attribute :service_records, Array[Preneeds::ServiceRecord] + attribute :service_records, Preneeds::ServiceRecord, array: true # (see Preneeds::BurialForm#as_eoas) # diff --git a/app/models/preneeds/virtus_base.rb b/app/models/preneeds/virtus_base.rb deleted file mode 100644 index b7284621c9d..00000000000 --- a/app/models/preneeds/virtus_base.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -module Preneeds - # Parent class for other Preneeds Burial form related models - # Should not be initialized directly - # - class VirtusBase - extend ActiveModel::Naming - include Virtus.model(nullify_blank: true) - - # Override `as_json` - # - # @param options [Hash] - # @see https://github.com/rails/rails/blob/49c613463b758a520a6162e702acc1158fc210ca/activesupport/lib/active_support/core_ext/object/json.rb#L46 - # - def as_json(options = {}) - super(options).deep_transform_keys { |key| key.camelize(:lower) } - end - end -end diff --git a/spec/requests/v0/preneeds/burial_forms_spec.rb b/spec/requests/v0/preneeds/burial_forms_spec.rb index d14318a28c1..9da5f6ec96e 100644 --- a/spec/requests/v0/preneeds/burial_forms_spec.rb +++ b/spec/requests/v0/preneeds/burial_forms_spec.rb @@ -16,7 +16,7 @@ let(:submission_record) { OpenStruct.new(application_uuid: 'UUID') } let(:form) do - Preneeds::BurialForm.new(params).tap do |f| + Preneeds::BurialForm.new(params[:application]).tap do |f| f.claimant = Preneeds::Claimant.new( email: 'foo@foo.com', name: Preneeds::FullName.new(