Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Extend Vets::Model with value "coercion" functionality #19979

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
57 changes: 16 additions & 41 deletions lib/vets/attributes/value.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'vets/types'

module Vets
module Attributes
class Value
Expand All @@ -14,49 +16,22 @@ def initialize(name, klass, array: false)
end

def setter_value(value)
validate_array(value) if @array
value = cast_boolean(value) if @klass == Bool
value = coerce_to_class(value)
validate_type(value)
value
type.cast(value)
end

private

def validate_array(value)
raise TypeError, "#{@name} must be an Array" unless value.is_a?(Array)

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

def cast_boolean(value)
ActiveModel::Type::Boolean.new.cast(value)
end

def coerce_to_class(value)
return value if value.is_a?(@klass) || value.nil?

if @klass == DateTime
begin
value = DateTime.parse(value) if value.is_a?(String)
rescue ArgumentError
raise TypeError, "#{@name} could not be parsed into a DateTime"
end
end

value.is_a?(Hash) ? @klass.new(value) : value
end

def validate_type(value)
return if (@array && value.is_a?(Array)) || value.is_a?(@klass) || value.nil?

raise TypeError, "#{@name} must be a #{@klass}"
# Acts as a "type factory"
def type
@type ||= if @array
Vets::Type::Array.new(@name, @klass)
elsif Vets::Type::Primitive::PRIMITIVE_TYPES.include?(@klass.name)
Vets::Type::Primitive.new(@name, @klass)
elsif @klass.module_parents.include?(Vets::Type)
@klass.new(@name, @klass)
elsif @klass == ::Hash
Vets::Type::Hash.new(@name)
else
Vets::Type::Object.new(@name, @klass)
end
end
end
end
Expand Down
1 change: 1 addition & 0 deletions lib/vets/model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ module Bool; end
class TrueClass; include Bool; end
class FalseClass; include Bool; end

# This will be a replacement for Common::Base
module Vets
module Model
extend ActiveSupport::Concern
Expand Down
39 changes: 39 additions & 0 deletions lib/vets/type/array.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# frozen_string_literal: true

require 'vets/types'

module Vets
module Type
class Array < Base
def self.primitive
::Array
end

def cast(value)
return nil if value.nil?

raise TypeError, "#{@name} must be an Array" unless value.is_a?(::Array)

casted_value = value.map { |item| type.cast(item) }

unless casted_value.all? { |item| item.is_a?(@klass.try(:primitive) || @klass) }
raise TypeError, "All elements of #{@name} must be of type #{@klass}"
end

casted_value
end

def type
@type ||= if Vets::Type::Primitive::PRIMITIVE_TYPES.include?(@klass.name)
Vets::Type::Primitive.new(@name, @klass)
elsif @klass.module_parents.include?(Vets::Type)
@klass.new(@name, @klass)
elsif @klass == ::Hash
Vets::Type::Hash.new(@name)
else
Vets::Type::Object.new(@name, @klass)
end
end
end
end
end
22 changes: 22 additions & 0 deletions lib/vets/type/base.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
# frozen_string_literal: true

# Dir['lib/vets/type/**/*.rb'].each { |file| require file.gsub('lib/', '') }

module Vets
module Type
class Base
def initialize(name, klass)
@name = name
@klass = klass
end

def cast(value)
raise NotImplementedError, "#{self.class} must implement #cast"
end

def self.primitive
raise NotImplementedError, "#{self.class} must implement #primitive"
end
end
end
end
21 changes: 21 additions & 0 deletions lib/vets/type/date_time_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'vets/type/base'

module Vets
module Type
class DateTimeString < Base
def self.primitive
::String
end

def cast(value)
return nil if value.nil?

Time.parse(value).iso8601
rescue ArgumentError
raise TypeError, "#{@name} is not Time parseable"
end
end
end
end
25 changes: 25 additions & 0 deletions lib/vets/type/hash.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# frozen_string_literal: true

require 'vets/type/base'

module Vets
module Type
class Hash < Base
def initialize(name)
super(name, ::Hash)
end

def self.primitive
::Hash
end

def cast(value)
return nil if value.nil?

raise TypeError, "#{@name} must be a Hash" unless value.is_a?(::Hash)

value
end
end
end
end
21 changes: 21 additions & 0 deletions lib/vets/type/http_date.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'vets/type/base'

module Vets
module Type
class HTTPDate < Base
def self.primitive
::String
end

def cast(value)
return nil if value.nil?

Time.parse(value.to_s).utc.httpdate
rescue ArgumentError
raise TypeError, "#{@name} is not Time parseable"
end
end
end
end
21 changes: 21 additions & 0 deletions lib/vets/type/iso8601_time.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'vets/type/base'

module Vets
module Type
class ISO8601Time < Base
def self.primitive
::String
end

def cast(value)
return nil if value.nil?

Time.iso8601(value)
rescue ArgumentError
raise TypeError, "#{@name} is not iso8601"
end
end
end
end
21 changes: 21 additions & 0 deletions lib/vets/type/object.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'vets/type/base'

module Vets
module Type
class Object < Base
def cast(value)
return nil if value.nil?

if value.is_a?(::Hash)
@klass.new(value)
elsif value.is_a?(@klass)
value
else
raise TypeError, "#{@name} must be a Hash or an instance of #{@klass}"
end
end
end
end
end
37 changes: 37 additions & 0 deletions lib/vets/type/primitive.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# frozen_string_literal: true

require 'vets/type/base'
require 'vets/model' # this is required for Bools

module Vets
module Type
class Primitive < Base
PRIMITIVE_TYPES = %w[String Integer Float Date Time DateTime Bool].freeze

def cast(value)
return value if value.is_a?(@klass) || value.nil?

begin
case @klass.name
when 'String' then ActiveModel::Type::String.new.cast(value)
when 'Integer' then ActiveModel::Type::Integer.new.cast(value)
when 'Float' then ActiveModel::Type::Float.new.cast(value)
when 'Date' then ActiveModel::Type::Date.new.cast(value)
when 'Time' then Time.zone.parse(value.to_s)
when 'DateTime' then ActiveModel::Type::DateTime.new.cast(value)
when 'Bool' then ActiveModel::Type::Boolean.new.cast(value)
else invalid_type!
end
rescue
invalid_type!
end
end

private

def invalid_type!
raise TypeError, "#{@name} could not be casted to #{@klass}"
end
end
end
end
19 changes: 19 additions & 0 deletions lib/vets/type/titlecase_string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
# frozen_string_literal: true

require 'vets/type/base'

module Vets
module Type
class TitlecaseString < Base
def self.primitive
::String
end

def cast(value)
return nil if value.nil?

value.to_s.downcase.titlecase
end
end
end
end
21 changes: 21 additions & 0 deletions lib/vets/type/utc_time.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'vets/type/base'

module Vets
module Type
class UTCTime < Base
def self.primitive
::Time
end

def cast(value)
return nil if value.nil?

Time.parse(value.to_s).utc
rescue ArgumentError
raise TypeError, "#{@name} is not Time parseable"
end
end
end
end
17 changes: 17 additions & 0 deletions lib/vets/types.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# frozen_string_literal: true

# These are the castable type and
# types that can be used by Vets::Attributes
# Primitive types include:
# String, Integer, Float, Date, Time, DateTime, Bool

require 'vets/type/base'
require 'vets/type/date_time_string'
require 'vets/type/hash'
require 'vets/type/http_date'
require 'vets/type/iso8601_time'
require 'vets/type/object'
require 'vets/type/primitive'
require 'vets/type/titlecase_string'
require 'vets/type/utc_time'
require 'vets/type/array'
10 changes: 5 additions & 5 deletions modules/avs/app/models/avs/v0/after_visit_summary.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ class V0::AfterVisitSummary

attribute :id, String
attribute :icn, String
attribute :meta, Object
attribute :patient_info, Object
attribute :meta, Hash
attribute :patient_info, Hash
attribute :appointment_iens, Array, default: []
attribute :clinics_visited, Array, default: []
attribute :providers, Array, default: []
Expand All @@ -29,14 +29,14 @@ class V0::AfterVisitSummary
attribute :problems, Array, default: []
attribute :clinical_reminders, Array, default: []
attribute :clinical_services, Array, default: []
attribute :allergies_reactions, Object
attribute :allergies_reactions, Hash
attribute :clinic_medications, Array, default: []
attribute :va_medications, Array, default: []
attribute :nonva_medications, Array, default: []
attribute :med_changes_summary, Object
attribute :med_changes_summary, Hash
attribute :lab_results, Array, default: []
attribute :radiology_reports1_yr, String
attribute :discrete_data, Object
attribute :discrete_data, Hash
attribute :more_help_and_information, String

def initialize(data)
Expand Down
Loading
Loading