Skip to content

Commit

Permalink
common -> vets
Browse files Browse the repository at this point in the history
  • Loading branch information
stevenjcumming committed Sep 23, 2024
1 parent 8faf831 commit 9fda986
Show file tree
Hide file tree
Showing 4 changed files with 151 additions and 114 deletions.
117 changes: 3 additions & 114 deletions app/models/preneeds/base.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# frozen_string_literal: true

require 'vets/model'

# This will be moved after virtus is removed
module Boolean; end
class TrueClass; include Boolean; end
Expand All @@ -9,103 +11,7 @@ class FalseClass; include Boolean; end
# 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

class Base < Vets::Model
# Override `as_json`
#
# @param options [Hash]
Expand All @@ -115,22 +21,5 @@ def attributes
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
55 changes: 55 additions & 0 deletions lib/vets/attributes.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# frozen_string_literal: true

module Vets
module Attributes
def self.included(base)
base.extend(ClassMethods)
base.instance_variable_set(:@attributes, {})
end

module ClassMethods

Check failure on line 11 in lib/vets/attributes.rb

View workflow job for this annotation

GitHub Actions / Linting and Security

Layout/EmptyLinesAroundModuleBody: Extra empty line detected at module body beginning.
# rubocop:disable ThreadSafety/InstanceVariableInClassMethod
def attributes
@attributes
end
# rubocop:enable ThreadSafety/InstanceVariableInClassMethod

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|
Vets::Attributes::Value.cast(name, klass, value, array)
end
end
end
end
end
53 changes: 53 additions & 0 deletions lib/vets/attributes/value.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
module Vets
module Attributes
class Value

def self.cast(name, klass, value, array: false)
new((name, klass, value, array)).setter_value(value)

Check failure on line 6 in lib/vets/attributes/value.rb

View workflow job for this annotation

GitHub Actions / Linting and Security

Lint/Syntax: unexpected token tRPAREN (Using Ruby 3.3 parser; configure using `TargetRubyVersion` parameter, under `AllCops`)
end

Check failure on line 7 in lib/vets/attributes/value.rb

View workflow job for this annotation

GitHub Actions / Linting and Security

Lint/Syntax: unexpected token kEND (Using Ruby 3.3 parser; configure using `TargetRubyVersion` parameter, under `AllCops`)

def initialize(name, klass, array: false)
@name = name
@klass_type = klass_type
@array = array
end

def setter_value(value)
validate_array(value) if @array
value = cast_boolean(value) if @klass_type == Boolean
value = coerce_to_class(value)
validate_type(value)
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_type.new(item) : item
end

unless value.all? { |item| item.is_a?(@klass_type) }
raise TypeError, "All elements of #{@name} must be of type #{@klass_type}"
end
end

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

def coerce_to_class(value)
value.is_a?(Hash) ? @klass_type.new(value) : value
end

def validate_type(value)
if (@array && value.is_a?(Array)) || value.is_a?(@klass_type) || value.nil?
return
end
raise TypeError, "#{@name} must be a #{@klass_type}"
end
end

Check failure on line 51 in lib/vets/attributes/value.rb

View workflow job for this annotation

GitHub Actions / Linting and Security

Lint/Syntax: unexpected token kEND (Using Ruby 3.3 parser; configure using `TargetRubyVersion` parameter, under `AllCops`)
end
end
40 changes: 40 additions & 0 deletions lib/vets/model.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# frozen_string_literal: true

module Vets
class Model
extend ActiveModel::Naming
include ActiveModel::Model
include ActiveModel::Serializers::JSON
include Vets::Attributes

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

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

0 comments on commit 9fda986

Please sign in to comment.