Skip to content

Instantly share code, notes, and snippets.

@pftg
Last active March 1, 2019 15:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pftg/f77eeb0014147d7a95745369a0df5354 to your computer and use it in GitHub Desktop.
Save pftg/f77eeb0014147d7a95745369a0df5354 to your computer and use it in GitHub Desktop.
Example of virtual attributes cases
# 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