-
Notifications
You must be signed in to change notification settings - Fork 62
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
8faf831
commit 9fda986
Showing
4 changed files
with
151 additions
and
114 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
|
||
# 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | ||
end | ||
|
||
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 | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |