Skip to content

Commit

Permalink
Merge pull request #79 from Mapotempo/add_dynamic_attribute
Browse files Browse the repository at this point in the history
Add dynamic attribute
  • Loading branch information
giallon committed Apr 29, 2024
2 parents a1a4ea6 + 3ce0517 commit 8d13347
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 2 deletions.
1 change: 1 addition & 0 deletions lib/couchbase-orm.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ module CouchbaseOrm
autoload :Document, 'couchbase-orm/base'
autoload :NestedDocument, 'couchbase-orm/base'
autoload :HasMany, 'couchbase-orm/utilities/has_many'
autoload :AttributesDynamic, 'couchbase-orm/attributes/dynamic'

def self.logger
@@logger ||= defined?(Rails) ? Rails.logger : Logger.new(STDOUT).tap { |l| l.level = Logger::INFO unless ENV["COUCHBASE_ORM_DEBUG"] }
Expand Down
99 changes: 99 additions & 0 deletions lib/couchbase-orm/attributes/dynamic.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# frozen_string_literal: true, encoding: ASCII-8BIT

module CouchbaseOrm
module AttributesDynamic
extend ActiveSupport::Concern

# Override respond_to? so it responds properly for dynamic attributes.
#
# @example Does this object respond to the method?
# person.respond_to?(:title)
#
# @param [ Array ] name The name of the method.
# @param [ true | false ] include_private
#
# @return [ true | false ] True if it does, false if not.
def respond_to?(name, include_private = false)
super || attributes&.key?(name.to_s.reader)
end

private

# Override private _assign_attribute to accept dynamic attribute
#
# @param [ String ] name of attribute
# @param [ Object ] value of attribute
#
# @return [ Object ] value of attribute
def _assign_attribute(name, value)
responds = name.reader == 'id' || respond_to?(name.writer)
if responds
public_send(name.writer, value)
else
type = value.class.to_s.underscore.to_sym
type = :hash if type == :"active_support/hash_with_indifferent_access"
type = ActiveModel::Type.lookup(type)
@attributes[name] = ActiveModel::Attribute.from_database(name, value, type)
end
end

# Define a reader method for a dynamic attribute.
#
# @example Define a reader method.
# model.define_dynamic_reader(:field)
#
# @param [ String ] name The name of the field.
def define_dynamic_reader(name)
return unless name.valid_method_name?

instance_eval do
define_singleton_method(name) do
@attributes[getter].value
end
end
end

# Define a writer method for a dynamic attribute.
#
# @example Define a writer method.
# model.define_dynamic_writer(:field)
#
# @param [ String ] name The name of the field.
def define_dynamic_writer(name)
return unless name.valid_method_name?

instance_eval do
define_singleton_method("#{name}=") do |value|
@attributes.write_from_user(name, value)
value
end
end
end

# Used for allowing accessor methods for dynamic attributes.
#
# @api private
#
# @example Call through method_missing.
# document.method_missing(:test)
#
# @param [ String | Symbol ] name The name of the method.
# @param [ Object... ] *args The arguments to the method.
#
# @return [ Object ] The result of the method call.
def method_missing(name, *args)
attr = name.to_s
return super unless attr.reader != 'id' && attributes.key?(attr.reader)

getter = attr.reader
if attr.writer?
define_dynamic_writer(getter)
@attributes.write_from_user(getter, args.first)
args.first
else
define_dynamic_reader(getter)
@attributes[getter].value
end
end
end
end
21 changes: 20 additions & 1 deletion lib/couchbase-orm/base.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
end
require 'active_support/hash_with_indifferent_access'
require 'couchbase'
require 'couchbase-orm/extensions/string'
require 'couchbase-orm/error'
require 'couchbase-orm/views'
require 'couchbase-orm/n1ql'
Expand Down Expand Up @@ -86,7 +87,17 @@ def attribute_for_inspect(attr_name)
value.inspect
end

if ActiveModel::VERSION::MAJOR < 6
if ActiveModel::VERSION::MAJOR <= 6
def format_for_inspect(value)
if value.is_a?(String) && value.length > 50
"#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
else
value.inspect
end
end

def attribute_names
self.class.attribute_names
end
Expand All @@ -104,6 +115,14 @@ def _write_attribute(attr_name, value)
@attributes.write_from_user(attr_name.to_s, value)
value
end

def read_attribute(attr_name, &block)
name = attr_name.to_s
name = self.class.attribute_aliases[name] || name

name = @primary_key if name == "id" && @primary_key
@attributes.fetch_value(name, &block)
end
end
end

Expand Down
27 changes: 27 additions & 0 deletions lib/couchbase-orm/extensions/string.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
module CouchbaseOrm
module Extensions
module String
def reader
delete("=").sub(/\_before\_type\_cast\z/, '')
end

def writer
sub(/\_before\_type\_cast\z/, '') + "="
end

def writer?
include?("=")
end

def before_type_cast?
ends_with?("_before_type_cast")
end

def valid_method_name?
/[@$"-]/ !~ self
end
end
end
end

::String.__send__(:include, CouchbaseOrm::Extensions::String)
2 changes: 1 addition & 1 deletion lib/couchbase-orm/types.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
require "couchbase-orm/types/nested"
require "couchbase-orm/types/encrypted"

if ActiveModel::VERSION::MAJOR < 6
if ActiveModel::VERSION::MAJOR <= 6
# In Rails 5, the type system cannot allow overriding the default types
ActiveModel::Type.registry.instance_variable_get(:@registrations).delete_if do |k|
k.matches?(:date) || k.matches?(:datetime) || k.matches?(:timestamp)
Expand Down
27 changes: 27 additions & 0 deletions spec/attribute_dynamic_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# frozen_string_literal: true, encoding: ASCII-8BIT

require File.expand_path("../support", __FILE__)

class AttributeDynamicTest < CouchbaseOrm::Base
include CouchbaseOrm::AttributesDynamic

attribute :name, :string
attribute :job, :string
end

describe CouchbaseOrm::AttributesDynamic do
context 'from initialize' do
it 'should accept unknown attribute from initialize' do
dynamic = AttributeDynamicTest.new(name: 'joe', new_attribute: 1)
expect(dynamic.new_attribute).to eq(1)
end
end

context 'from Couchbase' do
it 'should accept unknown attribute from Couchbase' do
dynamic = AttributeDynamicTest.create!(name: 'joe', new_attribute: 2)
expect(AttributeDynamicTest.find_by_id(dynamic.id)).to have_attributes(new_attribute: 2)
dynamic.destroy
end
end
end

0 comments on commit 8d13347

Please sign in to comment.