Last active
March 1, 2019 15:05
-
-
Save pftg/f77eeb0014147d7a95745369a0df5354 to your computer and use it in GitHub Desktop.
Example of virtual attributes cases
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
# frozen_string_literal: true | |
require 'bundler/inline' | |
gemfile(true) do | |
source 'https://rubygems.org' | |
git_source(:github) { |repo| "https://github.com/#{repo}.git" } | |
# Activate the gem you are reporting the issue against. | |
gem 'activerecord' | |
gem 'sqlite3', '~> 1.3.6' | |
end | |
require 'active_record' | |
require 'minitest/autorun' | |
require 'logger' | |
# This connection will do for database-independent bug reports. | |
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:') | |
ActiveRecord::Base.logger = Logger.new(STDOUT) | |
ActiveRecord::Schema.define do | |
create_table :posts, force: true do |t| | |
t.string :author_first_name | |
t.string :author_last_name | |
t.text :metadata | |
end | |
end | |
class Post < ActiveRecord::Base | |
# Virtual attributes with Ruby methods | |
attr_accessor :simple_virtual_attribute_without_type_cast | |
attr_reader :simple_virtual_attribute_with_type_cast | |
def simple_virtual_attribute_with_type_cast=(value) | |
@simple_virtual_attribute_with_type_cast = value.to_i | |
end | |
# Extends with Rails API to have more advanced features like type casting and dirty handling | |
attribute :virtual_attribute_with_type_cast, :boolean | |
# Use case of virtual attr: to on/off hooks | |
before_validation if: :virtual_attribute_with_type_cast do | |
self.simple_virtual_attribute_without_type_cast = 'Virtual attr has been set to true' | |
end | |
# Use case of virtual attr: decorator to store composite values | |
# Simple virtual attribute with pre-processing values and store in persistent attributes | |
def author_full_name | |
[author_first_name, author_last_name].join(' ') | |
end | |
def author_full_name=(name) | |
split = name.split(' ', 2) | |
self.author_first_name = split.first | |
self.author_last_name = split.last | |
end | |
# Custom type casting without introducing & registering new type in Rails | |
attribute :metadata_raw_json | |
serialize :metadata, Hash | |
validate :metadata_raw_json_valid_format, if: -> { @metadata_raw_json.present? } | |
def metadata_raw_json | |
@metadata_raw_json || metadata.to_json | |
end | |
def metadata_raw_json=(value) | |
@metadata_raw_json = value | |
self.metadata = build_metadata_from(@metadata_raw_json) | |
end | |
private | |
def build_metadata_from(json) | |
try_parse_metadata!(json) | |
rescue JSON::ParserError | |
nil | |
end | |
def metadata_raw_json_valid_format | |
errors.add(:metadata_raw_json, 'invalid JSON format') unless json?(@metadata_raw_json) | |
end | |
def json?(raw_json) | |
try_parse_metadata!(raw_json).present? | |
rescue JSON::ParserError | |
false | |
end | |
# @raise JSON::ParserError | |
def try_parse_metadata!(value) | |
ActiveSupport::JSON.decode(value) | |
end | |
end | |
class VirtualAttributesUseCase < Minitest::Test | |
def test_simple_virtual_attribute | |
assert_equal( | |
'1', | |
Post.new(simple_virtual_attribute_without_type_cast: '1') | |
.simple_virtual_attribute_without_type_cast | |
) | |
assert_equal( | |
1, | |
Post.new(simple_virtual_attribute_without_type_cast: 1) | |
.simple_virtual_attribute_without_type_cast | |
) | |
# No dirty enabled | |
refute Post.new.respond_to?(:simple_virtual_attribute_without_type_cast_changed?) | |
assert_equal( | |
1, | |
Post.new(simple_virtual_attribute_with_type_cast: '1') | |
.simple_virtual_attribute_with_type_cast | |
) | |
assert_equal( | |
1, | |
Post.new(simple_virtual_attribute_with_type_cast: 1) | |
.simple_virtual_attribute_with_type_cast | |
) | |
end | |
def test_simple_virtual_attribute_with_custom_typecast | |
assert_equal( | |
1, | |
Post.new(simple_virtual_attribute_with_type_cast: '1') | |
.simple_virtual_attribute_with_type_cast | |
) | |
assert_equal( | |
1, | |
Post.new(simple_virtual_attribute_with_type_cast: 1) | |
.simple_virtual_attribute_with_type_cast | |
) | |
end | |
def test_attribute_api | |
post = Post.new virtual_attribute_with_type_cast: '1' | |
assert_equal true, post.virtual_attribute_with_type_cast, 'type casting works' | |
assert post.virtual_attribute_with_type_cast_changed?, 'dirty module has been enabled' | |
end | |
# Most common usage of virtual attributes to pre-process new values before save | |
def test_virtual_attribute_with_custom_processing | |
post = Post.create! author_full_name: 'Paul Keen' | |
assert_equal 'Paul', post.author_first_name | |
assert_equal 'Keen', post.author_last_name | |
post = Post.create! author_first_name: 'Paul', author_last_name: 'Keen' | |
assert_equal 'Paul Keen', post.author_full_name | |
end | |
# Common usage of virtual attributes to enable/disable hooks | |
def test_virtual_attribute_in_hooks | |
post = Post.new virtual_attribute_with_type_cast: '1' | |
post.valid? | |
assert_equal 'Virtual attr has been set to true', post.simple_virtual_attribute_without_type_cast | |
end | |
# Some type casting operations could throw runtime errors or require some schema. | |
# For this we need more advance cases | |
def test_virtual_attributes_with_validation_input_before_cast | |
post = Post.new metadata_raw_json: 'invalid JSON document' | |
refute post.valid? | |
post = Post.create! metadata: { existed: 'json' } | |
post.update metadata_raw_json: 'invalid JSON document' | |
refute post.valid? | |
assert_equal({}, post.metadata) | |
assert post.errors[:metadata_raw_json].present? | |
post = Post.create! metadata: { existed: 'json' } | |
post.update metadata_raw_json: '{ "valid": "json" }' | |
assert post.valid? | |
assert 'json', post.metadata['valid'] | |
post = Post.new metadata_raw_json: '{ "valid": "json" }' | |
assert 'json', post.metadata['valid'] | |
post = Post.new metadata_raw_json: '{ "valid": "json" }', metadata: { invalid: 'false' } | |
assert 'json', post.metadata['valid'] | |
post = Post.new metadata: { invalid: 'false' }, metadata_raw_json: '{ "valid": "json" }' | |
assert 'json', post.metadata['valid'] | |
post = Post.new metadata: { invalid: 'false' } | |
assert_equal '{"invalid":"false"}', post.metadata_raw_json | |
end | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment